ForkedMavenExecutor.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.cling.executor.forked;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.apache.maven.api.cli.Executor;
import org.apache.maven.api.cli.ExecutorException;
import org.apache.maven.api.cli.ExecutorRequest;
import static java.util.Objects.requireNonNull;
import static org.apache.maven.api.cli.ExecutorRequest.getCanonicalPath;
/**
* Forked executor implementation, that spawns a subprocess with Maven from the installation directory. Very costly
* but provides the best isolation.
*/
public class ForkedMavenExecutor implements Executor {
protected final boolean useMavenArgsEnv;
public ForkedMavenExecutor() {
this(true);
}
public ForkedMavenExecutor(boolean useMavenArgsEnv) {
this.useMavenArgsEnv = useMavenArgsEnv;
}
@Override
public int execute(ExecutorRequest executorRequest) throws ExecutorException {
requireNonNull(executorRequest);
validate(executorRequest);
return doExecute(executorRequest);
}
@Override
public String mavenVersion(ExecutorRequest executorRequest) throws ExecutorException {
requireNonNull(executorRequest);
validate(executorRequest);
try {
Path cwd = Files.createTempDirectory("forked-executor-maven-version");
try {
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
int exitCode = execute(executorRequest.toBuilder()
.cwd(cwd)
.arguments(List.of("--version", "--quiet"))
.stdOut(stdout)
.build());
if (exitCode == 0) {
if (stdout.size() > 0) {
return stdout.toString()
.replace("\n", "")
.replace("\r", "")
.trim();
}
return UNKNOWN_VERSION;
} else {
throw new ExecutorException(
"Maven version query unexpected exitCode=" + exitCode + "\nLog: " + stdout);
}
} finally {
Files.deleteIfExists(cwd);
}
} catch (IOException e) {
throw new ExecutorException("Failed to determine maven version", e);
}
}
protected void validate(ExecutorRequest executorRequest) throws ExecutorException {}
protected int doExecute(ExecutorRequest executorRequest) throws ExecutorException {
ArrayList<String> cmdAndArguments = new ArrayList<>();
cmdAndArguments.add(executorRequest
.installationDirectory()
.resolve("bin")
.resolve(IS_WINDOWS ? executorRequest.command() + ".cmd" : executorRequest.command())
.toString());
String mavenArgsEnv = System.getenv("MAVEN_ARGS");
if (useMavenArgsEnv && mavenArgsEnv != null && !mavenArgsEnv.isEmpty()) {
Arrays.stream(mavenArgsEnv.split(" "))
.filter(s -> !s.trim().isEmpty())
.forEach(cmdAndArguments::add);
}
cmdAndArguments.addAll(executorRequest.arguments());
ArrayList<String> jvmArgs = new ArrayList<>();
if (!executorRequest.userHomeDirectory().equals(getCanonicalPath(Paths.get(System.getProperty("user.home"))))) {
jvmArgs.add("-Duser.home=" + executorRequest.userHomeDirectory().toString());
}
if (executorRequest.jvmArguments().isPresent()) {
jvmArgs.addAll(executorRequest.jvmArguments().get());
}
if (executorRequest.jvmSystemProperties().isPresent()) {
jvmArgs.addAll(executorRequest.jvmSystemProperties().get().entrySet().stream()
.map(e -> "-D" + e.getKey() + "=" + e.getValue())
.toList());
}
HashMap<String, String> env = new HashMap<>();
if (executorRequest.environmentVariables().isPresent()) {
env.putAll(executorRequest.environmentVariables().get());
}
if (!jvmArgs.isEmpty()) {
String mavenOpts = env.getOrDefault("MAVEN_OPTS", "");
if (!mavenOpts.isEmpty()) {
mavenOpts += " ";
}
mavenOpts += String.join(" ", jvmArgs);
env.put("MAVEN_OPTS", mavenOpts);
}
env.remove("MAVEN_ARGS"); // we already used it if configured to do so
if (executorRequest.skipMavenRc()) {
env.put("MAVEN_SKIP_RC", "true");
}
try {
ProcessBuilder pb = new ProcessBuilder()
.directory(executorRequest.cwd().toFile())
.command(cmdAndArguments);
if (!env.isEmpty()) {
pb.environment().putAll(env);
}
Process process = pb.start();
pump(process, executorRequest).await();
return process.waitFor();
} catch (IOException e) {
throw new ExecutorException("IO problem while executing command: " + cmdAndArguments, e);
} catch (InterruptedException e) {
throw new ExecutorException("Interrupted while executing command: " + cmdAndArguments, e);
}
}
protected CountDownLatch pump(Process p, ExecutorRequest executorRequest) {
CountDownLatch latch = new CountDownLatch(3);
String suffix = "-pump-" + p.pid();
Thread stdoutPump = new Thread(() -> {
try {
OutputStream stdout = executorRequest.stdOut().orElse(OutputStream.nullOutputStream());
p.getInputStream().transferTo(stdout);
stdout.flush();
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
latch.countDown();
}
});
stdoutPump.setName("stdout" + suffix);
stdoutPump.start();
Thread stderrPump = new Thread(() -> {
try {
OutputStream stderr = executorRequest.stdErr().orElse(OutputStream.nullOutputStream());
p.getErrorStream().transferTo(stderr);
stderr.flush();
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
latch.countDown();
}
});
stderrPump.setName("stderr" + suffix);
stderrPump.start();
Thread stdinPump = new Thread(() -> {
try {
OutputStream in = p.getOutputStream();
executorRequest.stdIn().orElse(InputStream.nullInputStream()).transferTo(in);
in.flush();
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
latch.countDown();
}
});
stdinPump.setName("stdin" + suffix);
stdinPump.start();
return latch;
}
}