All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.zeroturnaround.exec.ProcessExecutor Maven / Gradle / Ivy

There is a newer version: 1.12
Show newest version
/*
 * 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy