View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.cling.executor.forked;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.io.UncheckedIOException;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.concurrent.CountDownLatch;
34  
35  import org.apache.maven.api.cli.Executor;
36  import org.apache.maven.api.cli.ExecutorException;
37  import org.apache.maven.api.cli.ExecutorRequest;
38  
39  import static java.util.Objects.requireNonNull;
40  import static org.apache.maven.api.cli.ExecutorRequest.getCanonicalPath;
41  
42  /**
43   * Forked executor implementation, that spawns a subprocess with Maven from the installation directory. Very costly
44   * but provides the best isolation.
45   */
46  public class ForkedMavenExecutor implements Executor {
47      protected final boolean useMavenArgsEnv;
48  
49      public ForkedMavenExecutor() {
50          this(true);
51      }
52  
53      public ForkedMavenExecutor(boolean useMavenArgsEnv) {
54          this.useMavenArgsEnv = useMavenArgsEnv;
55      }
56  
57      @Override
58      public int execute(ExecutorRequest executorRequest) throws ExecutorException {
59          requireNonNull(executorRequest);
60          validate(executorRequest);
61  
62          return doExecute(executorRequest);
63      }
64  
65      @Override
66      public String mavenVersion(ExecutorRequest executorRequest) throws ExecutorException {
67          requireNonNull(executorRequest);
68          validate(executorRequest);
69          try {
70              Path cwd = Files.createTempDirectory("forked-executor-maven-version");
71              try {
72                  ByteArrayOutputStream stdout = new ByteArrayOutputStream();
73                  int exitCode = execute(executorRequest.toBuilder()
74                          .cwd(cwd)
75                          .arguments(List.of("--version", "--quiet"))
76                          .stdOut(stdout)
77                          .build());
78                  if (exitCode == 0) {
79                      if (stdout.size() > 0) {
80                          return stdout.toString()
81                                  .replace("\n", "")
82                                  .replace("\r", "")
83                                  .trim();
84                      }
85                      return UNKNOWN_VERSION;
86                  } else {
87                      throw new ExecutorException(
88                              "Maven version query unexpected exitCode=" + exitCode + "\nLog: " + stdout);
89                  }
90              } finally {
91                  Files.deleteIfExists(cwd);
92              }
93          } catch (IOException e) {
94              throw new ExecutorException("Failed to determine maven version", e);
95          }
96      }
97  
98      protected void validate(ExecutorRequest executorRequest) throws ExecutorException {}
99  
100     protected int doExecute(ExecutorRequest executorRequest) throws ExecutorException {
101         ArrayList<String> cmdAndArguments = new ArrayList<>();
102         cmdAndArguments.add(executorRequest
103                 .installationDirectory()
104                 .resolve("bin")
105                 .resolve(IS_WINDOWS ? executorRequest.command() + ".cmd" : executorRequest.command())
106                 .toString());
107 
108         String mavenArgsEnv = System.getenv("MAVEN_ARGS");
109         if (useMavenArgsEnv && mavenArgsEnv != null && !mavenArgsEnv.isEmpty()) {
110             Arrays.stream(mavenArgsEnv.split(" "))
111                     .filter(s -> !s.trim().isEmpty())
112                     .forEach(cmdAndArguments::add);
113         }
114 
115         cmdAndArguments.addAll(executorRequest.arguments());
116 
117         ArrayList<String> jvmArgs = new ArrayList<>();
118         if (!executorRequest.userHomeDirectory().equals(getCanonicalPath(Paths.get(System.getProperty("user.home"))))) {
119             jvmArgs.add("-Duser.home=" + executorRequest.userHomeDirectory().toString());
120         }
121         if (executorRequest.jvmArguments().isPresent()) {
122             jvmArgs.addAll(executorRequest.jvmArguments().get());
123         }
124         if (executorRequest.jvmSystemProperties().isPresent()) {
125             jvmArgs.addAll(executorRequest.jvmSystemProperties().get().entrySet().stream()
126                     .map(e -> "-D" + e.getKey() + "=" + e.getValue())
127                     .toList());
128         }
129 
130         HashMap<String, String> env = new HashMap<>();
131         if (executorRequest.environmentVariables().isPresent()) {
132             env.putAll(executorRequest.environmentVariables().get());
133         }
134         if (!jvmArgs.isEmpty()) {
135             String mavenOpts = env.getOrDefault("MAVEN_OPTS", "");
136             if (!mavenOpts.isEmpty()) {
137                 mavenOpts += " ";
138             }
139             mavenOpts += String.join(" ", jvmArgs);
140             env.put("MAVEN_OPTS", mavenOpts);
141         }
142         env.remove("MAVEN_ARGS"); // we already used it if configured to do so
143 
144         try {
145             ProcessBuilder pb = new ProcessBuilder()
146                     .directory(executorRequest.cwd().toFile())
147                     .command(cmdAndArguments);
148             if (!env.isEmpty()) {
149                 pb.environment().putAll(env);
150             }
151 
152             Process process = pb.start();
153             pump(process, executorRequest).await();
154             return process.waitFor();
155         } catch (IOException e) {
156             throw new ExecutorException("IO problem while executing command: " + cmdAndArguments, e);
157         } catch (InterruptedException e) {
158             throw new ExecutorException("Interrupted while executing command: " + cmdAndArguments, e);
159         }
160     }
161 
162     protected CountDownLatch pump(Process p, ExecutorRequest executorRequest) {
163         CountDownLatch latch = new CountDownLatch(3);
164         String suffix = "-pump-" + p.pid();
165         Thread stdoutPump = new Thread(() -> {
166             try {
167                 OutputStream stdout = executorRequest.stdOut().orElse(OutputStream.nullOutputStream());
168                 p.getInputStream().transferTo(stdout);
169                 stdout.flush();
170             } catch (IOException e) {
171                 throw new UncheckedIOException(e);
172             } finally {
173                 latch.countDown();
174             }
175         });
176         stdoutPump.setName("stdout" + suffix);
177         stdoutPump.start();
178         Thread stderrPump = new Thread(() -> {
179             try {
180                 OutputStream stderr = executorRequest.stdErr().orElse(OutputStream.nullOutputStream());
181                 p.getErrorStream().transferTo(stderr);
182                 stderr.flush();
183             } catch (IOException e) {
184                 throw new UncheckedIOException(e);
185             } finally {
186                 latch.countDown();
187             }
188         });
189         stderrPump.setName("stderr" + suffix);
190         stderrPump.start();
191         Thread stdinPump = new Thread(() -> {
192             try {
193                 OutputStream in = p.getOutputStream();
194                 executorRequest.stdIn().orElse(InputStream.nullInputStream()).transferTo(in);
195                 in.flush();
196             } catch (IOException e) {
197                 throw new UncheckedIOException(e);
198             } finally {
199                 latch.countDown();
200             }
201         });
202         stdinPump.setName("stdin" + suffix);
203         stdinPump.start();
204         return latch;
205     }
206 }