SubProcess.java
package net.morimekta.io.proc;
import net.morimekta.io.ForkingOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.ThreadFactory;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Utility for handling output from simple sub-process commands.
* <p>
* The utility expects the output to be {@code UTF-8} strings, and will have
* null output or error values if the given content is not parseable as {@code UTF-8}.
* <p>
* Simple usage is as follows:
* <pre>{@code
* SubProcess ls = SubProcess.newBuilder("ls", "-1")
* .withWorkingDir(Paths.get("/tmp"))
* .run();
* Arrays.stream(ls.getOutput().split("\n"))
* .filter(StringUtil:isNotEmpty)
* .sorted()
* .forEach(System.out::println);
* }</pre>
*/
public class SubProcess {
/**
* Get the process standard output as string. If the output was not compatible
* with {@code UTF-8} parsing, this will return null.
*
* @return The std out.
*/
public String getOutput() {
return output;
}
/**
* Get the process standard error as string. If the output was not compatible
* with {@code UTF-8} parsing, this will return null.
*
* @return The std err.
*/
public String getError() {
return error;
}
/**
* Get the process' exit code.
*
* @return The exit code.
*/
public int getExitCode() {
return exitCode;
}
/**
* Create a sub-process builder.
*
* @param cmd Command to be run.
* @return The builder.
*/
public static Runner newRunner(String... cmd) {
return new Runner(cmd);
}
/**
* Fluent runner class for simple subprocess handling.
*/
public static class Runner {
/**
* Set output stream to receive the error output from process.
* The stream will be forked between the {@link SubProcess#getError()}
* handling and the provided stream.
*
* @param err The error stream.
* @return The builder.
*/
public Runner withErr(OutputStream err) {
runner.setErr(new ForkingOutputStream(this.err, err));
return this;
}
/**
* Set output stream to receive the standard output from process.
* The stream will be forked between the {@link SubProcess#getOutput()}
* handling and the provided stream.
*
* @param out The output stream.
* @return The builder.
*/
public Runner withOut(OutputStream out) {
runner.setOut(new ForkingOutputStream(this.out, out));
return this;
}
/**
* Set input stream for the subprocess.
*
* @param in The input stream.
* @return The builder.
*/
public Runner withIn(InputStream in) {
this.in = in;
return this;
}
/**
* Add a set of variables to the environment for the process.
*
* @param key Env var key.
* @param value Env var value.
* @return The builder.
*/
public Runner withEnv(String key, String value) {
this.runner.addToEnv(Map.of(key, value));
return this;
}
/**
* @param workingDir The working dir of the process to be run in.
* @return The builder.
*/
public Runner withWorkingDir(Path workingDir) {
runner.setWorkingDir(workingDir);
return this;
}
/**
* @param runtime The runtime to use to run the subprocess.
* @return The builder.
*/
public Runner withRuntime(Runtime runtime) {
runner.setRuntime(runtime);
return this;
}
/**
* @param threadFactory Thread factory for creating threads for handling IO.
* @return The builder.
*/
public Runner withThreadFactory(ThreadFactory threadFactory) {
runner.setThreadFactory(threadFactory);
return this;
}
/**
* @param deadlineMs Deadline for the process to complete.
* @return The builder.
*/
public Runner withDeadlineMs(long deadlineMs) {
runner.setDeadlineMs(deadlineMs);
return this;
}
/**
* @param deadlineFlushMs Deadline for flushing IO streams.
* @return The builder.
*/
public Runner withDeadlineFlushMs(long deadlineFlushMs) {
runner.setDeadlineFlushMs(deadlineFlushMs);
return this;
}
/**
* Run the subprocess.
*
* @return The subprocess response.
* @throws IOException If executing failed.
*/
public SubProcess run() throws IOException {
int exit = runner.exec(in, command);
String error = null, output = null;
try {
error = err.toString(UTF_8);
} catch (IllegalArgumentException ignore) {}
try {
output = out.toString(UTF_8);
} catch (IllegalArgumentException ignore) {}
return new SubProcess(output, error, exit);
}
// --- Private ---
private final String[] command;
private final ByteArrayOutputStream out;
private final ByteArrayOutputStream err;
private final SubProcessRunner runner;
private InputStream in;
private Runner(String... cmd) {
runner = new SubProcessRunner();
command = cmd;
out = new ByteArrayOutputStream();
err = new ByteArrayOutputStream();
runner.setErr(err);
runner.setOut(out);
}
}
// --- Private ---
private final String output;
private final String error;
private final int exitCode;
private SubProcess(String output,
String error,
int exitCode) {
this.output = output;
this.error = error;
this.exitCode = exitCode;
}
}