org.zeroturnaround.exec.ProcessExecutor Maven / Gradle / Ivy
Show all versions of zt-exec Show documentation
/*
* Copyright (C) 2013 ZeroTurnaround
* Contains fragments of code from Apache Commons Exec, rights owned
* by Apache Software Foundation (ASF).
*
* Licensed 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.zeroturnaround.exec;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.io.output.TeeOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeroturnaround.exec.listener.CompositeProcessListener;
import org.zeroturnaround.exec.listener.DestroyerListenerAdapter;
import org.zeroturnaround.exec.listener.ProcessDestroyer;
import org.zeroturnaround.exec.listener.ProcessListener;
import org.zeroturnaround.exec.listener.ShutdownHookProcessDestroyer;
import org.zeroturnaround.exec.stream.CallerLoggerUtil;
import org.zeroturnaround.exec.stream.ExecuteStreamHandler;
import org.zeroturnaround.exec.stream.PumpStreamHandler;
import org.zeroturnaround.exec.stream.slf4j.Slf4jDebugOutputStream;
import org.zeroturnaround.exec.stream.slf4j.Slf4jInfoOutputStream;
/**
* Helper for executing sub processes.
*
* It's implemented as a wrapper of {@link ProcessBuilder} complementing it with additional features such as:
*
* - Handling process streams (copied from Commons Exec library).
* - Destroying process on VM exit (copied from Commons Exec library).
* - Checking process exit code.
* - Setting a timeout for running the process.
* - Either waiting for the process to finish ({@link #execute()}) or returning a {@link Future} ({@link #start()}.
* - Reading the process output stream into a buffer ({@link #readOutput(boolean)}, {@link ProcessResult}).
*
*
* The default configuration for executing a process is following:
*
* - Process is not automatically destroyed on VM exit.
* - Error stream is redirected to its output stream. Use {@link #redirectErrorStream(boolean)} to override it.
* - Output stream is pumped to a {@link NullOutputStream}, Use {@link #streams(ExecuteStreamHandler)}, {@link #redirectOutput(OutputStream)},
* {@link #info(Logger)}, {@link #info(String)}, {@link #debug(Logger)} or {@link #debug(String)} to override it.
* - Only
0
is allowed as an exit code. Use {@link #exitValues(Integer...)} to override it.
*
*
*
* @author Rein Raudjärv
* @see ProcessResult
*/
public class ProcessExecutor {
private static final Logger log = LoggerFactory.getLogger(ProcessExecutor.class);
public static final Integer[] DEFAULT_EXIT_VALUES = { 0 };
public static final boolean DEFAULT_REDIRECT_ERROR_STREAM = true;
/**
* Process builder used by this executor.
*/
private final ProcessBuilder builder = new ProcessBuilder();
/**
* Set of accepted exit codes or null
if all exit codes are allowed.
*/
private Set allowedExitValues;
/**
* Timeout for running a process. If the process is running too long a {@link TimeoutException} is thrown and the process is destroyed.
*/
private Long timeout;
private TimeUnit timeoutUnit;
/**
* Process stream Handler (copied from Commons Exec library). If null
streams are not handled.
*/
private ExecuteStreamHandler streams;
/**
* true
if the process output should be read to a buffer and returned by {@link ProcessResult#output()}.
*/
private boolean readOutput;
/**
* Process event handlers.
*/
private final CompositeProcessListener listeners = new CompositeProcessListener();
{
// Run in case of any constructor
exitValues(DEFAULT_EXIT_VALUES);
redirectOutput(null);
redirectError(null);
destroyer(null);
redirectErrorStream(DEFAULT_REDIRECT_ERROR_STREAM);
}
/**
* Creates new {@link ProcessExecutor} instance.
*/
public ProcessExecutor() {
}
/**
* Creates new {@link ProcessExecutor} instance for the given program and its arguments.
* @param command The list containing the program and its arguments.
*/
public ProcessExecutor(List command) {
command(command);
}
/**
* Creates new {@link ProcessExecutor} instance for the given program and its arguments.
* @param command A string array containing the program and its arguments.
*/
public ProcessExecutor(String... command) {
command(command);
}
/**
* Sets the program and its arguments which are being executed.
*
* @param command The list containing the program and its arguments.
* @return This process executor.
*/
public ProcessExecutor command(List command) {
builder.command(command);
return this;
}
/**
* Sets the program and its arguments which are being executed.
*
* @param command A string array containing the program and its arguments.
* @return This process executor.
*/
public ProcessExecutor command(String... command) {
builder.command(command);
return this;
}
/**
* Splits string by spaces and passes it to {@link ProcessExecutor#command(String...)}
*
* NB: this method do not handle whitespace escaping,
* "mkdir new\ folder"
would be interpreted as
* {"mkdir", "new\", "folder"}
command.
*
* @param commandWithArgs A string array containing the program and its arguments.
* @return This process executor.
*/
public ProcessExecutor commandSplit(String commandWithArgs) {
builder.command(commandWithArgs.split(" "));
return this;
}
/**
* Sets this working directory for the process being executed.
* The argument may be null
-- this means to use the
* working directory of the current Java process, usually the
* directory named by the system property user.dir
,
* as the working directory of the child process.
*
* @param directory The new working directory
* @return This process executor.
*/
public ProcessExecutor directory(File directory) {
builder.directory(directory);
return this;
}
/**
* Adds additional environment variables for the process being executed.
*
* @param env environment variables added to the process being executed.
* @return This process executor.
*/
public ProcessExecutor environment(Map env) {
builder.environment().putAll(env);
return this;
}
/**
* Sets this process executor's redirectErrorStream
property.
*
* If this property is true
, then any error output generated by subprocesses will be merged with the standard output.
* This makes it easier to correlate error messages with the corresponding output.
* The initial value is true
.
*
* @param redirectErrorStream The new property value
* @return This process executor.
*/
public ProcessExecutor redirectErrorStream(boolean redirectErrorStream) {
builder.redirectErrorStream(redirectErrorStream);
return this;
}
/**
* Allows any exit value for the process being executed.
*
* @return This process executor.
*/
public ProcessExecutor exitValueAny() {
return exitValues((Integer[]) null);
}
/**
* Sets the allowed exit value for the process being executed.
*
* @param exitValue single exit value or null
if all exit values are allowed.
* @return This process executor.
*/
public ProcessExecutor exitValue(Integer exitValue) {
return exitValues(exitValue == null ? null : new Integer[] { exitValue } );
}
/**
* Sets the allowed exit values for the process being executed.
*
* @param exitValues set of exit values or null
if all exit values are allowed.
* @return This process executor.
*/
public ProcessExecutor exitValues(Integer... exitValues) {
allowedExitValues = exitValues == null ? null : new HashSet(Arrays.asList(exitValues));
return this;
}
/**
* Sets the allowed exit values for the process being executed.
*
* @param exitValues set of exit values or null
if all exit values are allowed.
* @return This process executor.
*/
public ProcessExecutor exitValues(int[] exitValues) {
if (exitValues == null)
return exitValueAny();
// Convert int[] -> Integer[]
Integer[] array = new Integer[exitValues.length];
for (int i = 0; i < array.length; i++)
array[i] = exitValues[i];
return exitValues(array);
}
/**
* Sets a timeout for the process being executed. When this timeout is reached a {@link TimeoutException} is thrown and the process is destroyed.
* This only applies to execute
methods not start
methods.
*
* @param timeout timeout for running a process.
* @return This process executor.
*/
public ProcessExecutor timeout(long timeout, TimeUnit unit) {
this.timeout = timeout;
this.timeoutUnit = unit;
return this;
}
/**
* @return current stream handler for the process being executed.
*/
public ExecuteStreamHandler streams() {
return streams;
}
/**
* Sets a stream handler for the process being executed.
* @return This process executor.
*/
public ProcessExecutor streams(ExecuteStreamHandler streams) {
validateStreams(streams, readOutput);
this.streams = streams;
return this;
}
/**
* Redirects the process' output stream to given output stream.
* If this method is invoked multiple times each call overwrites the previous.
* Use {@link #redirectOutputAlsoTo(OutputStream)} if you want to redirect the output to multiple streams.
*
* @param output output stream where the process output is redirected to (null
means {@link NullOutputStream} which acts like a /dev/null
).
* @return This process executor.
*/
public ProcessExecutor redirectOutput(OutputStream output) {
if (output == null)
output = NullOutputStream.NULL_OUTPUT_STREAM;
PumpStreamHandler pumps = pumps();
// Only set the output stream handler, preserve the same error stream handler
return streams(new PumpStreamHandler(output, pumps == null ? null : pumps.getErr(), pumps == null ? null : pumps.getInput()));
}
/**
* Redirects the process' error stream to given output stream.
* If this method is invoked multiple times each call overwrites the previous.
* Use {@link #redirectErrorAlsoTo(OutputStream)} if you want to redirect the error to multiple streams.
*
* Calling this method automatically disables merging the process error stream to its output stream.
*
*
* @param output output stream where the process error is redirected to (null
means {@link NullOutputStream} which acts like a /dev/null
).
* @return This process executor.
*/
public ProcessExecutor redirectError(OutputStream output) {
if (output == null)
output = NullOutputStream.NULL_OUTPUT_STREAM;
PumpStreamHandler pumps = pumps();
// Only set the error stream handler, preserve the same output stream handler
streams(new PumpStreamHandler(pumps == null ? null : pumps.getOut(), output, pumps == null ? null : pumps.getInput()));
redirectErrorStream(false);
return this;
}
/**
* Redirects the process' output stream also to a given output stream.
* This method can be used to redirect output to multiple streams.
*
* @return This process executor.
*/
public ProcessExecutor redirectOutputAlsoTo(OutputStream output) {
return streams(redirectOutputAlsoTo(pumps(), output));
}
/**
* Redirects the process' error stream also to a given output stream.
* This method can be used to redirect error to multiple streams.
*
* Calling this method automatically disables merging the process error stream to its output stream.
*
*
* @return This process executor.
*/
public ProcessExecutor redirectErrorAlsoTo(OutputStream output) {
streams(redirectErrorAlsoTo(pumps(), output));
redirectErrorStream(false);
return this;
}
/**
* @return current PumpStreamHandler (maybe null
).
* @throws IllegalStateException if the current stream handler is not an instance of {@link PumpStreamHandler}.
*
* @see #streams()
*/
public PumpStreamHandler pumps() {
if (streams == null)
return null;
if (!(streams instanceof PumpStreamHandler))
throw new IllegalStateException("Only PumpStreamHandler is supported.");
return (PumpStreamHandler) streams;
}
/**
* Redirects the process' output stream also to a given output stream.
*
* @return new stream handler created.
*/
private static PumpStreamHandler redirectOutputAlsoTo(PumpStreamHandler pumps, OutputStream output) {
if (output == null)
throw new IllegalArgumentException("OutputStream must be provided.");
OutputStream current = pumps.getOut();
if (current != null && !(current instanceof NullOutputStream)) {
output = new TeeOutputStream(current, output);
}
return new PumpStreamHandler(output, pumps.getErr(), pumps.getInput());
}
/**
* Redirects the process' error stream also to a given output stream.
*
* @return new stream handler created.
*/
private static PumpStreamHandler redirectErrorAlsoTo(PumpStreamHandler pumps, OutputStream output) {
if (output == null)
throw new IllegalArgumentException("OutputStream must be provided.");
OutputStream current = pumps.getErr();
if (current != null && !(current instanceof NullOutputStream)) {
output = new TeeOutputStream(current, output);
}
return new PumpStreamHandler(pumps.getOut(), output, pumps.getInput());
}
/**
* Sets this process executor's readOutput
property.
*
* If this property is true
,
* the process output should be read to a buffer and returned by {@link ProcessResult#output()}.
* The initial value is false
.
*
* @param readOutput The new property value
* @return This process executor.
*/
public ProcessExecutor readOutput(boolean readOutput) {
validateStreams(streams, readOutput);
this.readOutput = readOutput;
return this;
}
/**
* Validates that if readOutput
is true
the output could be read with the given {@link ExecuteStreamHandler} instance.
*/
private void validateStreams(ExecuteStreamHandler streams, boolean readOutput) {
if (readOutput && !(streams instanceof PumpStreamHandler))
throw new IllegalStateException("Only PumpStreamHandler is supported if readOutput is true.");
}
/**
* Logs the process' output to a given {@link Logger} with info
level.
* @return This process executor.
* @deprecated use {@link #redirectOutputAsInfo(Logger)
*/
public ProcessExecutor info(Logger log) {
return redirectOutput(new Slf4jInfoOutputStream(log));
}
/**
* Logs the process' output to a given {@link Logger} with debug
level.
* @return This process executor.
* @deprecated use {@link #redirectOutputAsDebug(Logger)
*/
public ProcessExecutor debug(Logger log) {
return redirectOutput(new Slf4jDebugOutputStream(log));
}
/**
* Logs the process' output to a {@link Logger} with given name using info
level.
* @return This process executor.
* @deprecated use {@link #redirectOutputAsInfo(String)
*/
public ProcessExecutor info(String name) {
return info(getCallerLogger(name));
}
/**
* Logs the process' output to a {@link Logger} with given name using debug
level.
* @return This process executor.
* @deprecated use {@link #redirectOutputAsDebug(String)
*/
public ProcessExecutor debug(String name) {
return debug(getCallerLogger(name));
}
/**
* Logs the process' output to a {@link Logger} of the caller class using info
level.
* @return This process executor.
* @deprecated use {@link #redirectOutputAsInfo()}
*/
public ProcessExecutor info() {
return info(getCallerLogger(null));
}
/**
* Logs the process' output to a {@link Logger} of the caller class using debug
level.
* @return This process executor.
* @deprecated use {@link #redirectOutputAsDebug()}
*/
public ProcessExecutor debug() {
return debug(getCallerLogger(null));
}
/**
* Logs the process' output to a given {@link Logger} with info
level.
* @return This process executor.
*/
public ProcessExecutor redirectOutputAsInfo(Logger log) {
return redirectOutput(new Slf4jInfoOutputStream(log));
}
/**
* Logs the process' output to a given {@link Logger} with debug
level.
* @return This process executor.
*/
public ProcessExecutor redirectOutputAsDebug(Logger log) {
return redirectOutput(new Slf4jDebugOutputStream(log));
}
/**
* Logs the process' output to a {@link Logger} with given name using info
level.
* @return This process executor.
*/
public ProcessExecutor redirectOutputAsInfo(String name) {
return info(getCallerLogger(name));
}
/**
* Logs the process' output to a {@link Logger} with given name using debug
level.
* @return This process executor.
*/
public ProcessExecutor redirectOutputAsDebug(String name) {
return redirectOutputAsDebug(getCallerLogger(name));
}
/**
* Logs the process' output to a {@link Logger} of the caller class using info
level.
* @return This process executor.
*/
public ProcessExecutor redirectOutputAsInfo() {
return redirectOutputAsInfo(getCallerLogger(null));
}
/**
* Logs the process' output to a {@link Logger} of the caller class using debug
level.
* @return This process executor.
*/
public ProcessExecutor redirectOutputAsDebug() {
return redirectOutputAsDebug(getCallerLogger(null));
}
/**
* Logs the process' error to a given {@link Logger} with info
level.
* @return This process executor.
*/
public ProcessExecutor redirectErrorAsInfo(Logger log) {
return redirectError(new Slf4jInfoOutputStream(log));
}
/**
* Logs the process' error to a given {@link Logger} with debug
level.
* @return This process executor.
*/
public ProcessExecutor redirectErrorAsDebug(Logger log) {
return redirectError(new Slf4jDebugOutputStream(log));
}
/**
* Logs the process' error to a {@link Logger} with given name using info
level.
* @return This process executor.
*/
public ProcessExecutor redirectErrorAsInfo(String name) {
return redirectErrorAsInfo(getCallerLogger(name));
}
/**
* Logs the process' error to a {@link Logger} with given name using debug
level.
* @return This process executor.
*/
public ProcessExecutor redirectErrorAsDebug(String name) {
return redirectErrorAsDebug(getCallerLogger(name));
}
/**
* Logs the process' error to a {@link Logger} of the caller class using info
level.
* @return This process executor.
*/
public ProcessExecutor redirectErrorAsInfo() {
return redirectErrorAsInfo(getCallerLogger(null));
}
/**
* Logs the process' error to a {@link Logger} of the caller class using debug
level.
* @return This process executor.
*/
public ProcessExecutor redirectErrorAsDebug() {
return redirectErrorAsDebug(getCallerLogger(null));
}
/**
* Creates a {@link Logger} for the {@link ProcessExecutor}'s caller class.
*
* @param name name of the logger.
* @return SLF4J Logger instance.
*/
private Logger getCallerLogger(String name) {
return LoggerFactory.getLogger(CallerLoggerUtil.getName(name, 2));
}
/**
* Sets the process destroyer to be notified when the process starts and stops.
* @param destroyer helper for destroying all processes on certain event such as VM exit (maybe null
).
*
* @return This process executor.
*/
public ProcessExecutor destroyer(ProcessDestroyer destroyer) {
return listener(destroyer == null ? null : new DestroyerListenerAdapter(destroyer));
}
/**
* Sets the started process to be destroyed on VM exit (shutdown hooks are executed).
* If this VM gets killed the started process may not get destroyed.
*
* To undo this command call destroyer(null)
.
*
* @return This process executor.
*/
public ProcessExecutor destroyOnExit() {
return destroyer(ShutdownHookProcessDestroyer.INSTANCE);
}
/**
* Unregister all existing process event handlers and register new one.
* @param listener process event handler to be set (maybe null
).
*
* @return This process executor.
*/
public ProcessExecutor listener(ProcessListener listener) {
clearListeners();
if (listener != null)
addListener(listener);
return this;
}
/**
* Register new process event handler.
* @param listener process event handler to be added.
*
* @return This process executor.
*/
public ProcessExecutor addListener(ProcessListener listener) {
listeners.add(listener);
return this;
}
/**
* Unregister existing process event handler.
* @param listener process event handler to be removed.
*
* @return This process executor.
*/
public ProcessExecutor removeListener(ProcessListener listener) {
listeners.remove(listener);
return this;
}
/**
* Unregister all existing process event handlers.
*
* @return This process executor.
*/
public ProcessExecutor clearListeners() {
listeners.clear();
return this;
}
/**
* Executes the sub process. This method waits until the process exits, a timeout occurs or the caller thread gets interrupted.
* In the latter cases the process gets destroyed as well.
*
* @return exit code of the finished process.
* @throws IOException an error occurred when process was started or stopped.
* @throws InterruptedException this thread was interrupted.
* @throws TimeoutException timeout set by {@link #timeout(long, TimeUnit)} was reached.
* @throws InvalidExitValueException if invalid exit value was returned (@see {@link #exitValues(Integer...)}).
*/
public ProcessResult execute() throws IOException, InterruptedException, TimeoutException, InvalidExitValueException {
return waitFor(startInternal());
}
/**
* Start the sub process. This method does not wait until the process exits.
* Value passed to {@link #timeout(long, TimeUnit)} is ignored.
* Use {@link Future#get()} to wait for the process to finish.
* Invoke future.cancel(true);
to destroy the process.
*
* @return Future representing the exit value of the finished process.
* @throws IOException an error occurred when process was started.
*/
public StartedProcess start() throws IOException {
WaitForProcess task = startInternal();
ExecutorService service = Executors.newSingleThreadScheduledExecutor();
Future future = service.submit(task);
// Previously submitted tasks are executed but no new tasks will be accepted.
service.shutdown();
return new StartedProcess(task.getProcess(), future);
}
/**
* Start the process and its stream handlers.
*
* @return process the started process.
* @throws IOException the process or its stream handlers couldn't start (in the latter case we also destroy the process).
*/
private WaitForProcess startInternal() throws IOException {
// Invoke listeners - they can modify this executor
listeners.beforeStart(this);
if (builder.command().isEmpty())
throw new IllegalStateException("Command has not been set.");
validateStreams(streams, readOutput);
log.debug("Executing {}...", builder.command());
Process process;
try {
process = builder.start();
}
catch (IOException e) {
log.error("Could not start process:", e);
throw e;
}
log.debug("Started {}", process);
if (readOutput) {
PumpStreamHandler pumps = (PumpStreamHandler) streams;
ByteArrayOutputStream out = new ByteArrayOutputStream();
return startInternal(process, redirectOutputAlsoTo(pumps, out), out);
}
else {
return startInternal(process, streams, null);
}
}
private WaitForProcess startInternal(Process process, ExecuteStreamHandler streams, ByteArrayOutputStream out) throws IOException {
if (streams != null) {
try {
streams.setProcessInputStream(process.getOutputStream());
streams.setProcessOutputStream(process.getInputStream());
if (!builder.redirectErrorStream())
streams.setProcessErrorStream(process.getErrorStream());
}
catch (IOException e) {
process.destroy();
throw e;
}
streams.start();
}
Set exitValues = allowedExitValues == null ? null : new HashSet(allowedExitValues);
WaitForProcess result = new WaitForProcess(process, exitValues, streams, out, listeners.clone());
// Invoke listeners - changing this executor does not affect the started process any more
listeners.afterStart(process, this);
return result;
}
/**
* Wait until the process stops, a timeout occurs and the caller thread gets interrupted.
* In the latter cases the process gets destroyed as well.
*/
private ProcessResult waitFor(WaitForProcess task) throws IOException, InterruptedException, TimeoutException {
ProcessResult result;
if (timeout == null) {
// Use the current thread
result = task.call();
}
else {
// Fork another thread to invoke Process.waitFor()
ExecutorService service = Executors.newSingleThreadScheduledExecutor();
try {
result = service.submit(task).get(timeout, timeoutUnit);
}
catch (ExecutionException e) {
Throwable c = e.getCause();
if (c instanceof IOException)
throw (IOException) c;
if (c instanceof InterruptedException)
throw (InterruptedException) c;
if (c instanceof InvalidExitValueException)
throw (InvalidExitValueException) c;
throw new IllegalStateException("Error occured while waiting for process to finish:", c);
}
catch (TimeoutException e) {
log.debug("{} is running too long", task);
throw e;
}
finally {
// Interrupt the task if it's still running and release the ExecutorService's resources
service.shutdownNow();
}
}
return result;
}
}