DefaultExecutor.java
/*
* 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 org.apache.commons.exec;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.function.Supplier;
import org.apache.commons.exec.launcher.CommandLauncher;
import org.apache.commons.exec.launcher.CommandLauncherFactory;
/**
* The default class to start a subprocess. The implementation allows to
* <ul>
* <li>set a current working directory for the subprocess</li>
* <li>provide a set of environment variables passed to the subprocess</li>
* <li>capture the subprocess output of stdout and stderr using an ExecuteStreamHandler</li>
* <li>kill long-running processes using an ExecuteWatchdog</li>
* <li>define a set of expected exit values</li>
* <li>terminate any started processes when the main process is terminating using a ProcessDestroyer</li>
* </ul>
*
* The following example shows the basic usage:
*
* <pre>
* Executor exec = DefaultExecutor.builder().get();
* CommandLine cl = new CommandLine("ls -l");
* int exitvalue = exec.execute(cl);
* </pre>
*/
public class DefaultExecutor implements Executor {
/**
* Constructs a new builder.
*
* @param <T> The builder type.
* @since 1.4.0
*/
public static class Builder<T extends Builder<T>> implements Supplier<DefaultExecutor> {
private ThreadFactory threadFactory;
private ExecuteStreamHandler executeStreamHandler;
private File workingDirectory;
@SuppressWarnings("unchecked")
T asThis() {
return (T) this;
}
/**
* Creates a new configured DefaultExecutor.
*
* @return a new configured DefaultExecutor.
*/
@Override
public DefaultExecutor get() {
return new DefaultExecutor(threadFactory, executeStreamHandler, workingDirectory);
}
ExecuteStreamHandler getExecuteStreamHandler() {
return executeStreamHandler;
}
ThreadFactory getThreadFactory() {
return threadFactory;
}
File getWorkingDirectory() {
return workingDirectory;
}
/**
* Sets the PumpStreamHandler.
*
* @param executeStreamHandler the ExecuteStreamHandler, null resets to the default.
* @return this.
*/
public T setExecuteStreamHandler(final ExecuteStreamHandler executeStreamHandler) {
this.executeStreamHandler = executeStreamHandler;
return asThis();
}
/**
* Sets the ThreadFactory.
*
* @param threadFactory the ThreadFactory, null resets to the default.
* @return this.
*/
public T setThreadFactory(final ThreadFactory threadFactory) {
this.threadFactory = threadFactory;
return asThis();
}
/**
* Sets the working directory.
*
* @param workingDirectory the working directory., null resets to the default.
* @return this.
*/
public T setWorkingDirectory(final File workingDirectory) {
this.workingDirectory = workingDirectory;
return asThis();
}
}
/**
* Creates a new builder.
*
* @return a new builder.
* @since 1.4.0
*/
public static Builder<?> builder() {
return new Builder<>();
}
/** Taking care of output and error stream. */
private ExecuteStreamHandler executeStreamHandler;
/** The working directory of the process. */
private File workingDirectory;
/** Monitoring of long running processes. */
private ExecuteWatchdog watchdog;
/** The exit values considered to be successful. */
private int[] exitValues;
/** Launches the command in a new process. */
private final CommandLauncher launcher;
/** Optional cleanup of started processes. */
private ProcessDestroyer processDestroyer;
/** Worker thread for asynchronous execution. */
private Thread executorThread;
/** The first exception being caught to be thrown to the caller. */
private IOException exceptionCaught;
/**
* The thread factory.
*/
private final ThreadFactory threadFactory;
/**
* Constructs a default {@code PumpStreamHandler} and sets the working directory of the subprocess to the current working directory.
*
* The {@code PumpStreamHandler} pumps the output of the subprocess into our {@code System.out} and {@code System.err} to avoid into our {@code System.out}
* and {@code System.err} to avoid a blocked or deadlocked subprocess (see {@link Process Process}).
*
* @deprecated Use {@link Builder#get()}.
*/
@Deprecated
public DefaultExecutor() {
this(Executors.defaultThreadFactory(), new PumpStreamHandler(), new File("."));
}
DefaultExecutor(final ThreadFactory threadFactory, final ExecuteStreamHandler executeStreamHandler, final File workingDirectory) {
this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory();
this.executeStreamHandler = executeStreamHandler != null ? executeStreamHandler : new PumpStreamHandler();
this.workingDirectory = workingDirectory != null ? workingDirectory : new File(".");
this.launcher = CommandLauncherFactory.createVMLauncher();
this.exitValues = new int[0];
}
private void checkWorkingDirectory() throws IOException {
checkWorkingDirectory(workingDirectory);
}
private void checkWorkingDirectory(final File directory) throws IOException {
if (directory != null && !directory.exists()) {
throw new IOException(directory + " doesn't exist.");
}
}
/**
* Closes the Closeable, remembering any exception.
*
* @param closeable the {@link Closeable} to close.
*/
private void closeCatch(final Closeable closeable) {
try {
closeable.close();
} catch (final IOException e) {
setExceptionCaught(e);
}
}
/**
* Closes the streams belonging to the given Process.
*
* @param process the {@link Process}.
*/
@SuppressWarnings("resource")
private void closeProcessStreams(final Process process) {
closeCatch(process.getInputStream());
closeCatch(process.getOutputStream());
closeCatch(process.getErrorStream());
}
/**
* Creates a thread waiting for the result of an asynchronous execution.
*
* @param runnable the runnable passed to the thread.
* @param name the name of the thread.
* @return the thread
*/
protected Thread createThread(final Runnable runnable, final String name) {
return ThreadUtil.newThread(threadFactory, runnable, name, false);
}
/**
* @see org.apache.commons.exec.Executor#execute(CommandLine)
*/
@Override
public int execute(final CommandLine command) throws ExecuteException, IOException {
return execute(command, (Map<String, String>) null);
}
/**
* @see org.apache.commons.exec.Executor#execute(CommandLine, org.apache.commons.exec.ExecuteResultHandler)
*/
@Override
public void execute(final CommandLine command, final ExecuteResultHandler handler) throws ExecuteException, IOException {
execute(command, null, handler);
}
/**
* @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map)
*/
@Override
public int execute(final CommandLine command, final Map<String, String> environment) throws ExecuteException, IOException {
checkWorkingDirectory();
return executeInternal(command, environment, workingDirectory, executeStreamHandler);
}
/**
* @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map, org.apache.commons.exec.ExecuteResultHandler)
*/
@Override
public void execute(final CommandLine command, final Map<String, String> environment, final ExecuteResultHandler handler)
throws ExecuteException, IOException {
checkWorkingDirectory();
if (watchdog != null) {
watchdog.setProcessNotStarted();
}
executorThread = createThread(() -> {
int exitValue = Executor.INVALID_EXITVALUE;
try {
exitValue = executeInternal(command, environment, workingDirectory, executeStreamHandler);
handler.onProcessComplete(exitValue);
} catch (final ExecuteException e) {
handler.onProcessFailed(e);
} catch (final Exception e) {
handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e));
}
}, "CommonsExecDefaultExecutor");
getExecutorThread().start();
}
/**
* Execute an internal process. If the executing thread is interrupted while waiting for the child process to return the child process will be killed.
*
* @param command the command to execute.
* @param environment the execution environment.
* @param workingDirectory the working directory.
* @param streams process the streams (in, out, err) of the process.
* @return the exit code of the process.
* @throws IOException executing the process failed.
*/
private int executeInternal(final CommandLine command, final Map<String, String> environment, final File workingDirectory,
final ExecuteStreamHandler streams) throws IOException {
final Process process;
exceptionCaught = null;
try {
process = launch(command, environment, workingDirectory);
} catch (final IOException e) {
if (watchdog != null) {
watchdog.failedToStart(e);
}
throw e;
}
try {
setStreams(streams, process);
} catch (final IOException e) {
process.destroy();
if (watchdog != null) {
watchdog.failedToStart(e);
}
throw e;
}
streams.start();
try {
// add the process to the list of those to destroy if the VM exits
if (getProcessDestroyer() != null) {
getProcessDestroyer().add(process);
}
// associate the watchdog with the newly created process
if (watchdog != null) {
watchdog.start(process);
}
int exitValue = Executor.INVALID_EXITVALUE;
try {
exitValue = process.waitFor();
} catch (final InterruptedException e) {
process.destroy();
} finally {
// see http://bugs.sun.com/view_bug.do?bug_id=6420270
// see https://issues.apache.org/jira/browse/EXEC-46
// Process.waitFor should clear interrupt status when throwing InterruptedException
// but we have to do that manually
Thread.interrupted();
}
if (watchdog != null) {
watchdog.stop();
}
try {
streams.stop();
} catch (final IOException e) {
setExceptionCaught(e);
}
closeProcessStreams(process);
if (getExceptionCaught() != null) {
throw getExceptionCaught();
}
if (watchdog != null) {
try {
watchdog.checkException();
} catch (final IOException e) {
throw e;
} catch (final Exception e) {
throw new IOException(e);
}
}
if (isFailure(exitValue)) {
throw new ExecuteException("Process exited with an error: " + exitValue, exitValue);
}
return exitValue;
} finally {
// remove the process to the list of those to destroy if the VM exits
if (getProcessDestroyer() != null) {
getProcessDestroyer().remove(process);
}
}
}
/**
* Gets the first IOException being thrown.
*
* @return the first IOException being caught.
*/
private IOException getExceptionCaught() {
return exceptionCaught;
}
/**
* Gets the worker thread being used for asynchronous execution.
*
* @return the worker thread.
*/
protected Thread getExecutorThread() {
return executorThread;
}
/**
* @see org.apache.commons.exec.Executor#getProcessDestroyer()
*/
@Override
public ProcessDestroyer getProcessDestroyer() {
return processDestroyer;
}
/**
* @see org.apache.commons.exec.Executor#getStreamHandler()
*/
@Override
public ExecuteStreamHandler getStreamHandler() {
return executeStreamHandler;
}
/**
* Gets the thread factory. Z
*
* @return the thread factory.
*/
ThreadFactory getThreadFactory() {
return threadFactory;
}
/**
* @see org.apache.commons.exec.Executor#getWatchdog()
*/
@Override
public ExecuteWatchdog getWatchdog() {
return watchdog;
}
/**
* @see org.apache.commons.exec.Executor#getWorkingDirectory()
*/
@Override
public File getWorkingDirectory() {
return workingDirectory;
}
/** @see org.apache.commons.exec.Executor#isFailure(int) */
@Override
public boolean isFailure(final int exitValue) {
if (exitValues == null) {
return false;
}
if (exitValues.length == 0) {
return launcher.isFailure(exitValue);
}
for (final int exitValue2 : exitValues) {
if (exitValue2 == exitValue) {
return false;
}
}
return true;
}
/**
* Creates a process that runs a command.
*
* @param command the command to run.
* @param env the environment for the command.
* @param workingDirectory the working directory for the command.
* @return the process started.
* @throws IOException forwarded from the particular launcher used.
*/
protected Process launch(final CommandLine command, final Map<String, String> env, final File workingDirectory) throws IOException {
if (launcher == null) {
throw new IllegalStateException("CommandLauncher can not be null");
}
checkWorkingDirectory(workingDirectory);
return launcher.exec(command, env, workingDirectory);
}
/**
* Sets the first IOException thrown.
*
* @param e the IOException.
*/
private void setExceptionCaught(final IOException e) {
if (exceptionCaught == null) {
exceptionCaught = e;
}
}
/** @see org.apache.commons.exec.Executor#setExitValue(int) */
@Override
public void setExitValue(final int value) {
setExitValues(new int[] { value });
}
/** @see org.apache.commons.exec.Executor#setExitValues(int[]) */
@Override
public void setExitValues(final int[] values) {
exitValues = values == null ? null : (int[]) values.clone();
}
/**
* @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer)
*/
@Override
public void setProcessDestroyer(final ProcessDestroyer processDestroyer) {
this.processDestroyer = processDestroyer;
}
/**
* @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ExecuteStreamHandler)
*/
@Override
public void setStreamHandler(final ExecuteStreamHandler streamHandler) {
this.executeStreamHandler = streamHandler;
}
@SuppressWarnings("resource")
private void setStreams(final ExecuteStreamHandler streams, final Process process) throws IOException {
streams.setProcessInputStream(process.getOutputStream());
streams.setProcessOutputStream(process.getInputStream());
streams.setProcessErrorStream(process.getErrorStream());
}
/**
* @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog)
*/
@Override
public void setWatchdog(final ExecuteWatchdog watchdog) {
this.watchdog = watchdog;
}
/**
* Sets the working directory.
*
* @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File)
* @deprecated Use {@link Builder#setWorkingDirectory(File)}.
*/
@Deprecated
@Override
public void setWorkingDirectory(final File workingDirectory) {
this.workingDirectory = workingDirectory;
}
}