package com.spotify.spawn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import jnr.posix.POSIXFactory;
import static java.lang.ProcessBuilder.Redirect.PIPE;
import static java.util.Arrays.asList;
/**
* Utility for spawning subprocesses that terminate when the parent (the spawning process) exits.
*/
public class Subprocesses {
private static final Logger log = LoggerFactory.getLogger(Subprocesses.class);
public static final int OK_EXIT_CODE = 0;
public static final int INVALID_ARGUMENTS_EXIT_CODE = 2;
public static final int EXCEPTION_EXIT_CODE = 3;
public static final int SUBPROCESS_EXIT_CODE = 4;
/**
* Create a builder for a new {@link Subprocess}.
*/
public static SubprocessBuilder process() {
return new SubprocessBuilder();
}
public static class SubprocessBuilder {
private final ProcessBuilder processBuilder = new ProcessBuilder();
private OutputStream stdoutPipe;
private OutputStream stderrPipe;
private InputStream stdinPipe;
public SubprocessBuilder() {
}
private Integer parentExitCodeOnSubprocessExit;
private List<String> jvmArgs = Collections.emptyList();
private String main;
private List<String> args = Collections.emptyList();
/**
* Set an exit code that the parent (this) process should exit with if the subprocess exits. {@code null} to not
* terminate the parent on subprocess exit.
*/
public SubprocessBuilder terminateParentOnSubprocessExit(final Integer exitCode) {
this.parentExitCodeOnSubprocessExit = exitCode;
return this;
}
/**
* Terminate the parent on subprocess exit with exit code {@link Subprocesses#SUBPROCESS_EXIT_CODE}
*/
public SubprocessBuilder terminateParentOnSubprocessExit() {
return terminateParentOnSubprocessExit(SUBPROCESS_EXIT_CODE);
}
/**
* Set subprocess work directory.
*/
public SubprocessBuilder directory(final String directory) {
return directory(Paths.get(directory));
}
/**
* Set subprocess work directory.
*/
public SubprocessBuilder directory(final Path directory) {
return directory(directory.toFile());
}
/**
* Set subprocess work directory.
*/
public SubprocessBuilder directory(final File directory) {
processBuilder.directory(directory);
return this;
}
/**
* Set arguments to be passed the subprocess JVM.
*/
public SubprocessBuilder jvmArgs(final String... args) {
return jvmArgs(asList(args));
}
/**
* Set arguments to be passed the subprocess JVM.
*/
private SubprocessBuilder jvmArgs(final List<String> args) {
this.jvmArgs = new ArrayList<>(args);
return this;
}
/**
* Set the main class to be executed by the subprocess JVM.
*/
public SubprocessBuilder main(final Class<?> main) {
if (findMain(main) == null) {
throw new IllegalArgumentException("Main method not found in class: " + main);
}
return main(main.getName());
}
/**
* Set the main class to be executed by the subprocess JVM.
*/
private SubprocessBuilder main(final String main) {
this.main = main;
return this;
}
/**
* Set the arguments to be passed to the main method of the subprocess.
*/
public SubprocessBuilder args(final String... args) {
return args(asList(args));
}
/**
* Set the arguments to be passed to the main method of the subprocess.
*/
public SubprocessBuilder args(final List<String> args) {
this.args = new ArrayList<>(args);
return this;
}
public SubprocessBuilder inheritIO() {
processBuilder.inheritIO();
return this;
}
public SubprocessBuilder redirectStdout(final ProcessBuilder.Redirect redirect) {
processBuilder.redirectInput(redirect);
return this;
}
public SubprocessBuilder redirectStderr(final ProcessBuilder.Redirect redirect) {
processBuilder.redirectError(redirect);
return this;
}
public SubprocessBuilder redirectStdin(final ProcessBuilder.Redirect redirect) {
processBuilder.redirectOutput(redirect);
return this;
}
/**
* Pipe stdout from the subprocess to a target {@link OutputStream} on a background thread.
*/
public SubprocessBuilder pipeStdout(final OutputStream stream) {
redirectStdout(PIPE);
this.stdoutPipe = stream;
return this;
}
/**
* Pipe stderr from the subprocess to a target {@link OutputStream} on a background thread.
*/
public SubprocessBuilder pipeStderr(final OutputStream stream) {
redirectStderr(PIPE);
this.stderrPipe = stream;
return this;
}
/**
* Pipe an {@link InputStream} into stdin of the subprocess on a background thread.
*/
public SubprocessBuilder pipeStdin(final InputStream stream) {
redirectStdin(PIPE);
this.stdinPipe = stream;
return this;
}
/**
* Access the underlying {@link ProcessBuilder} to break the glass and configure it directly.
*/
public ProcessBuilder processBuilder() {
return processBuilder;
}
/**
* Spawn a new subprocess using these parameters.
*/
public Subprocess spawn() throws IOException {
final List<String> cmd = new ArrayList<>();
cmd.add(System.getProperty("java.home") + "/bin/java");
cmd.addAll(jvmArgs);
cmd.add("-cp");
cmd.add(System.getProperty("java.class.path"));
cmd.add(Trampoline.class.getName());
cmd.add(String.valueOf(pid()));
cmd.add(main);
cmd.addAll(args);
processBuilder.command(cmd);
final Process process = processBuilder.start();
if (stdoutPipe != null && processBuilder.redirectInput() == PIPE) {
pipe(process.getInputStream(), stdoutPipe);
}
if (stderrPipe != null && processBuilder.redirectError() == PIPE) {
pipe(process.getErrorStream(), stderrPipe);
}
if (stdinPipe != null && processBuilder.redirectOutput() == PIPE) {
pipe(stdinPipe, process.getOutputStream());
}
return new Subprocess(process, main, parentExitCodeOnSubprocessExit);
}
}
static Method findMain(final Class<?> cls) {
for (final Method method : cls.getMethods()) {
int mod = method.getModifiers();
if (method.getName().equals("main") &&
Modifier.isPublic(mod) && Modifier.isStatic(mod) &&
method.getParameterTypes().length == 1 &&
method.getParameterTypes()[0].equals(String[].class)) {
return method;
}
}
return null;
}
static int pid() {
return POSIXFactory.getPOSIX().getpid();
}
static void pipe(final InputStream in, final OutputStream out) {
final byte[] buf = new byte[4096];
startDaemon(() -> {
try {
final int n = in.read(buf);
if (n == -1) {
return;
}
out.write(buf, 0, n);
} catch (IOException e) {
log.warn("pipe error", e);
}
});
}
static void startDaemon(final Runnable task) {
final Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
private Subprocesses() {
throw new UnsupportedOperationException();
}
}