SubProcess.java
/*
* Copyright (c) 2020, Stein Eldar Johnsen
*
* 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 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.newRunner("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 single environment variable 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;
}
/**
* Set the working directory for the subprocess.
*
* @param workingDir The working directory path.
* @return The builder.
*/
public Runner withWorkingDir(Path workingDir) {
runner.setWorkingDir(workingDir);
return this;
}
/**
* Set the runtime used to execute the subprocess.
*
* @param runtime The runtime to use.
* @return The builder.
*/
public Runner withRuntime(Runtime runtime) {
runner.setRuntime(runtime);
return this;
}
/**
* Set the thread factory for creating threads that handle IO piping.
*
* @param threadFactory The thread factory to use.
* @return The builder.
*/
public Runner withThreadFactory(ThreadFactory threadFactory) {
runner.setThreadFactory(threadFactory);
return this;
}
/**
* Set a deadline in milliseconds for the process to complete.
*
* @param deadlineMs The deadline in milliseconds.
* @return The builder.
*/
public Runner withDeadlineMs(long deadlineMs) {
runner.setDeadlineMs(deadlineMs);
return this;
}
/**
* Set a deadline in milliseconds for flushing IO streams after the
* process exits.
*
* @param deadlineFlushMs The flush deadline in milliseconds.
* @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;
}
}