com.github.mike10004.nativehelper.subprocess.Subprocess Maven / Gradle / Ivy
/*
* The MIT License
*
* Copyright 2015 mchaberski.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.github.mike10004.nativehelper.subprocess;
import com.github.mike10004.nativehelper.subprocess.StreamContext.UniformStreamContext;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.io.ByteSource;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import org.apache.commons.lang3.StringUtils;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Objects.requireNonNull;
/**
* Class that represents a subprocess to be executed. Instances of this class
* are immutable and may be reused. This API adheres to an asynchronous model,
* so after you launch a process, you receive a {@link ProcessMonitor monitor}
* instance that allows you to wait for the result on the current thread or
* attach a listener notified when the process terminates.
*
* To launch a process and ignore the output:
*
* {@code
* ProcessMonitor, ?> monitor = Subprocess.running("true").build()
* .launcher(ProcessTracker.create())
* .launch();
* ProcessResult, ?> result = monitor.await();
* System.out.println("exit with status " + result.exitCode()); // exit with status 0
* }
*
*
* To launch a process and capture the output as strings:
*
* {@code
* ProcessMonitor monitor = Subprocess.running("echo")
* .arg("hello, world")
* .build()
* .launcher(ProcessTracker.create())
* .outputStrings(Charset.defaultCharset())
* .launch();
* ProcessResult result = monitor.await();
* System.out.println(result.content().stdout()); // hello, world
* }
*
*
* To launch a process and capture the output in files:
*
* {@code
* ProcessMonitor monitor = Subprocess.running("echo")
* .arg("this is in a file")
* .build()
* .launcher(ProcessTracker.create())
* .outputTempFiles(new File(System.getProperty("java.io.tmpdir")).toPath())
* .launch();
* ProcessResult result = monitor.await();
* System.out.println("printed:");
* java.nio.file.Files.copy(result.content().stdout().toPath(), System.out); // this is in a file
* }
*
*
* To launch a process and send it data on standard input:
*
* {@code
* ByteSource input = Files.asByteSource(new File("/etc/passwd"));
* ProcessMonitor monitor = Subprocess.running("grep")
* .arg("root")
* .build()
* .launcher(ProcessTracker.create())
* .outputStrings(Charset.defaultCharset(), input)
* .launch();
* ProcessResult result = monitor.await();
* System.out.println("printed " + result.content().stdout()); // root:x:0:0:root:/root:/bin/bash
* }
*
*
* @since 7.1.0
*/
public class Subprocess {
private final String executable;
private final ImmutableList arguments;
@Nullable
private final File workingDirectory;
private final ImmutableMap environment;
private final Supplier extends ListeningExecutorService> launchExecutorServiceFactory;
protected Subprocess(String executable, @Nullable File workingDirectory, Map environment, Iterable arguments) {
this.executable = requireNonNull(executable, "executable");
this.workingDirectory = workingDirectory;
this.arguments = ImmutableList.copyOf(arguments);
this.environment = ImmutableMap.copyOf(environment);
launchExecutorServiceFactory = ExecutorServices.newSingleThreadExecutorServiceFactory("subprocess-launch");
}
/**
* Launches a subprocess. Use {@link #launcher(ProcessTracker)} to build a launcher
* and invoke {@link Launcher#launch()} for a more fluent way of executing this method.
* @param processTracker process tracker to use
* @param streamContext stream context
* @param stream control type
* @param type of captured standard output content
* @param type of captured standard error content
* @return a process monitor
* @throws ProcessException if the process cannot be launched
*/
public ProcessMonitor launch(ProcessTracker processTracker, StreamContext streamContext) throws ProcessException {
C streamControl;
try {
streamControl = streamContext.produceControl();
} catch (IOException e) {
throw new ProcessLaunchException("failed to produce output context", e);
}
// a one-time use executor service; it is shutdown immediately after exactly one task is submitted
ListeningExecutorService launchExecutorService = launchExecutorServiceFactory.get();
ProcessMissionControl.Execution execution = new ProcessMissionControl(this, processTracker, launchExecutorService)
.launch(streamControl, exitCode -> {
StreamContent content = streamContext.transform(exitCode, streamControl);
return ProcessResult.direct(exitCode, content);
});
ListenableFuture> fullResultFuture = execution.getFuture();
launchExecutorService.shutdown(); // previously submitted tasks are executed
ProcessMonitor monitor = new BasicProcessMonitor<>(execution.getProcess(), fullResultFuture, processTracker);
return monitor;
}
@SuppressWarnings("unused")
public static class ProcessExecutionException extends ProcessException {
public ProcessExecutionException(String message) {
super(message);
}
public ProcessExecutionException(String message, Throwable cause) {
super(message, cause);
}
public ProcessExecutionException(Throwable cause) {
super(cause);
}
}
/**
* Class that represents a builder of program instances. Create a builder
* instance with {@link Subprocess#running(String) }.
* @see Subprocess
*/
@NotThreadSafe
@SuppressWarnings("UnusedReturnValue")
public static class Builder {
protected final String executable;
protected File workingDirectory;
protected final List arguments;
protected final Map environment;
/**
* Constructs a builder instance.
* @param executable the executable name or pathname of the executable file
*/
protected Builder(String executable) {
this.executable = checkNotNull(executable);
checkArgument(!executable.isEmpty(), "executable must be non-empty string");
arguments = new ArrayList<>();
environment = new LinkedHashMap<>();
}
/**
* Launch the process from this working directory.
* @param workingDirectory the directory
* @return this builder instance
*/
public Builder from(File workingDirectory) {
this.workingDirectory = checkNotNull(workingDirectory);
return this;
}
/**
* Set variables in the environment of the process to be executed.
* Each entry in the argument map is put into this builder's environment map.
* Existing entries are not cleared; use {@link #clearEnv()} for that.
* @param environment map of variables to set
* @return this builder instance
*/
public Builder env(Map environment) {
this.environment.putAll(environment);
return this;
}
/**
* Set variable in the environment of the process to be executed.
* @param name variable name
* @param value variable value
* @return this builder instance
*/
public Builder env(String name, String value) {
environment.put(name, checkNotNull(value, "value must be non-null"));
return this;
}
/**
* Clears this builder's environment map.
* @return this instance
*/
@SuppressWarnings("unused")
public Builder clearEnv() {
this.environment.clear();
return this;
}
/**
* Clears the argument list of this builder.
* @return this builder instance
*/
@SuppressWarnings("unused")
public Builder clearArgs() {
this.arguments.clear();
return this;
}
/**
* Appends an argument to the argument list of this builder.
* @param argument the argument
* @return this builder instance
*/
public Builder arg(String argument) {
this.arguments.add(checkNotNull(argument));
return this;
}
/**
* Appends arguments to the argument list of this builder.
* @param firstArgument the first argument to append
* @param otherArguments the other arguments to append
* @return this builder instance
* @see #args(Iterable)
*/
public Builder args(String firstArgument, String...otherArguments) {
return args(Lists.asList(checkNotNull(firstArgument), otherArguments));
}
/**
* Appends arguments to the argument list of this builder.
* @param arguments command line arguments
* @return this builder instance
*/
public Builder args(Iterable arguments) {
//noinspection ConstantConditions // inspector thinks Objects::nonNull will always return true
checkArgument(Iterables.all(arguments, Objects::nonNull), "all arguments must be non-null");
Iterables.addAll(this.arguments, arguments);
return this;
}
public Subprocess build() {
return new Subprocess(executable, workingDirectory, environment, arguments);
}
}
/**
* Creates a new launcher in the given process context. The launcher
* created does not specify that output is to be captured. Use the
* {@code Launcher} methods to specify how input is to be sent to the process and how
* output is to be captured.
* @param processTracker the process context
* @return the launcher
*/
public Launcher launcher(ProcessTracker processTracker) {
return toSinkhole(processTracker);
}
private Launcher toSinkhole(ProcessTracker processTracker) {
return new Launcher(processTracker, StreamContexts.sinkhole()){};
}
/**
* Helper class that retains references to some dependencies so that you can
* use a builder-style pattern to launch a process. Instances of this class are immutable
* and methods that have return type {@code Launcher} return new instances.
* @param standard output capture type
* @param standard error capture type
*/
public abstract class Launcher {
private final ProcessTracker processTracker;
protected final StreamContext, SO, SE> streamContext;
private Launcher(ProcessTracker processTracker, StreamContext, SO, SE> streamContext) {
this.processTracker = requireNonNull(processTracker);
this.streamContext = requireNonNull(streamContext);
}
/**
* Return a new uniform launcher that uses the given stream context.
* @param streamContext the stream context of the new launcher
* @param type of captured standard output and standard error
* @return a new launcher instance
*/
public UniformLauncher output(UniformStreamContext, S> streamContext) {
return uniformOutput(streamContext);
}
/**
* Return a new uniform launcher that uses the given stream context.
* @param streamContext the stream context of the new launcher
* @param type of captured standard output and standard error
* @return a new launcher instance
*/
public UniformLauncher uniformOutput(StreamContext, S, S> streamContext) {
return new UniformLauncher(processTracker, streamContext) {};
}
/**
* Return a new launcher that uses the given stream context.
* @param streamContext the stream context of the new launcher
* @param type of standard output content captured by the new launcher
* @param type of standard error content captured by the new launcher
* @return a new launcher instance
*/
public Launcher output(StreamContext, SO2, SE2> streamContext) {
return new Launcher(processTracker, streamContext) {};
}
/**
* Launches the process.
* @return the process monitor
* @throws ProcessException if there is an error that prevents the process from being launched
*/
public ProcessMonitor launch() throws ProcessException {
return Subprocess.this.launch(processTracker, streamContext);
}
/**
* Returns a new launcher that maps this launcher's output.
* @param stdoutMap function that maps standard output content
* @param stderrMap function that maps standard error content
* @param type of mapped standard output content
* @param type of mapped standard error content
* @return a new launcher instance
*/
public Launcher map(Function super SO, SO2> stdoutMap, Function super SE, SE2> stderrMap) {
return output(streamContext.map(stdoutMap, stderrMap));
}
/**
* Returns a new launcher that captures process standard output and error as strings.
* The specified characer encoding is used to decode the bytes collected from the
* process standard output and standard error streams.
* @param charset encoding of bytes on the process standard output and error streams
* @return a new launcher instance
* @see #outputStrings(Charset, ByteSource)
*/
public UniformLauncher outputStrings(Charset charset) {
requireNonNull(charset, "charset");
return outputStrings(charset, null);
}
/**
* Returns a new launcher that captures process standard output and error as strings.
* The specified characer encoding is used to decode the bytes collected from the
* process standard output and standard error streams.
* @param charset encoding of bytes on the process standard output and error streams
* @param stdin source providing bytes to be written on process standard input stream; may be null
* @return a new launcher instance
*/
public UniformLauncher outputStrings(Charset charset, @Nullable ByteSource stdin) {
requireNonNull(charset, "charset");
return output(StreamContexts.strings(charset, stdin));
}
/**
* Returns a new launcher that captures process standard output and error as byte arrays.
* @return a new launcher instance
* @see #outputInMemory(ByteSource)
*/
@SuppressWarnings("unused")
public UniformLauncher outputInMemory() {
return outputInMemory(null);
}
/**
* Returns a new launcher that captures process standard output and error as byte arrays.
* @param stdin source providing bytes to be written on process standard input stream; may be null
* @return a new launcher instance
*/
public UniformLauncher outputInMemory(@Nullable ByteSource stdin) {
UniformStreamContext, byte[]> m = StreamContexts.byteArrays(stdin);
return output(m);
}
/**
* Returns a new launcher that pipes process output to the JVM standard output and errors streams and
* pipes input from the JVM standard input stream to the process standard input stream.
* @return a new launcher instance
*/
@SuppressWarnings("unused")
public Launcher inheritAllStreams() {
return output(StreamContexts.inheritAll());
}
/**
* Returns a new launcher that pipes process output to the JVM standard output and errors streams
* but does not write anything on the process standard input stream.
* @return a new launcher instance
*/
public Launcher inheritOutputStreams() {
return output(StreamContexts.inheritOutputs());
}
/**
* Returns a new launcher that captures the process standard output and error content
* in files.
* @param stdoutFile the file to which standard output content is to be written
* @param stderrFile the file to which standard error content is to be written
* @param stdin source providing bytes to be written on process standard input stream; may be null
* @return a new launcher instance
*/
public UniformLauncher outputFiles(File stdoutFile, File stderrFile, @Nullable ByteSource stdin) {
return output(StreamContexts.outputFiles(stdoutFile, stderrFile, stdin));
}
/**
* Returns a new launcher that captures the process standard output and error content
* in files.
* @param stdoutFile the file to which standard output content is to be written
* @param stderrFile the file to which standard error content is to be written
* @return a new launcher instance
* @see #outputFiles(File, File, ByteSource)
*/
public UniformLauncher outputFiles(File stdoutFile, File stderrFile) {
return outputFiles(stdoutFile, stderrFile, null);
}
/**
* Returns a new launcher that captures the process standard output and error content
* in new, uniquely-named files created in the given directory.
* @param directory pathname of a existing directory in which files are to be created
* @return a new launcher instance
*/
public UniformLauncher outputTempFiles(Path directory) {
return outputTempFiles(directory, null);
}
/**
* Returns a new launcher that captures the process standard output and error content
* in new, uniquely-named files created in the given directory.
* @param directory pathname of a existing directory in which files are to be created
* @param stdin source providing bytes to be written on process standard input stream; may be null
* @return a new launcher instance
*/
public UniformLauncher outputTempFiles(Path directory, @Nullable ByteSource stdin) {
return output(StreamContexts.outputTempFiles(directory, stdin));
}
}
/**
* Class that represents a launcher using a uniform output control.
* @param type of captured standard output and standard error content
*/
public abstract class UniformLauncher extends Launcher {
private UniformLauncher(ProcessTracker processTracker, StreamContext, S, S> streamContext) {
super(processTracker, streamContext);
}
/**
* Returns a new launcher that maps captured standard output and standard error
* content to a different type.
* @param mapper map function
* @param destination type
* @return a new launcher instance
*/
public UniformLauncher map(Function super S, T> mapper) {
UniformStreamContext, S> u = UniformStreamContext.wrap(this.streamContext);
UniformStreamContext, T> t = u.map(mapper);
return uniformOutput(t);
}
}
/**
* Constructs a builder instance that will produce a program that
* launches the given executable. Checks that a file exists at the given pathname
* and that it is executable by the operating system.
* @param executable the executable file
* @return a builder instance
* @throws IllegalArgumentException if {@link File#canExecute() } is false
*/
public static Builder running(File executable) {
checkArgument(executable.isFile(), "file not found: %s", executable);
checkArgument(executable.canExecute(), "executable.canExecute");
return running(executable.getPath());
}
/**
* Constructs a builder instance that will produce a program that
* launches an executable corresponding to the argument string.
* @param executable the name of an executable or a path to a file
* @return a builder instance
*/
public static Builder running(String executable) {
return new Builder(executable);
}
/**
* Gets the executable to be executed.
* @return the executable
*/
@SuppressWarnings("unused")
public String executable() {
return executable;
}
/**
* Gets the arguments to be provided to the process.
* @return the arguments
*/
@SuppressWarnings("unused")
public ImmutableList arguments() {
return arguments;
}
/**
* Gets the working directory of the process. Null means the working
* directory of the JVM.
* @return the working directory pathname
*/
@Nullable
protected File workingDirectory() {
return workingDirectory;
}
/**
* Gets the map of environment variables to override or supplement
* the process environment.
* @return the environment
*/
@SuppressWarnings("unused")
public ImmutableMap environment() {
return environment;
}
@Override
public String toString() {
return "Subprocess{" +
"executable='" + executable + '\'' +
", arguments=" + StringUtils.abbreviateMiddle(String.valueOf(arguments), "...", 64) +
", workingDirectory=" + workingDirectory +
", environment=" + StringUtils.abbreviateMiddle(String.valueOf(environment), "...", 64) +
'}';
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy