| /* |
| * 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 org.apache.commons.exec.launcher.CommandLauncher; |
| import org.apache.commons.exec.launcher.CommandLauncherFactory; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Map; |
| |
| /** |
| * 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 = new DefaultExecutor(); |
| * CommandLine cl = new CommandLine("ls -l"); |
| * int exitvalue = exec.execute(cl); |
| * </pre> |
| * |
| */ |
| public class DefaultExecutor implements Executor { |
| |
| /** taking care of output and error stream */ |
| private ExecuteStreamHandler streamHandler; |
| |
| /** 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; |
| |
| /** |
| * Default constructor creating 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 java.lang.Process Process}). |
| */ |
| public DefaultExecutor() { |
| this.streamHandler = new PumpStreamHandler(); |
| this.launcher = CommandLauncherFactory.createVMLauncher(); |
| this.exitValues = new int[0]; |
| this.workingDirectory = new File("."); |
| this.exceptionCaught = null; |
| } |
| |
| /** |
| * @see org.apache.commons.exec.Executor#getStreamHandler() |
| */ |
| @Override |
| public ExecuteStreamHandler getStreamHandler() { |
| return streamHandler; |
| } |
| |
| /** |
| * @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ExecuteStreamHandler) |
| */ |
| @Override |
| public void setStreamHandler(final ExecuteStreamHandler streamHandler) { |
| this.streamHandler = streamHandler; |
| } |
| |
| /** |
| * @see org.apache.commons.exec.Executor#getWatchdog() |
| */ |
| @Override |
| public ExecuteWatchdog getWatchdog() { |
| return watchdog; |
| } |
| |
| /** |
| * @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog) |
| */ |
| @Override |
| public void setWatchdog(final ExecuteWatchdog watchDog) { |
| this.watchdog = watchDog; |
| } |
| |
| /** |
| * @see org.apache.commons.exec.Executor#getProcessDestroyer() |
| */ |
| @Override |
| public ProcessDestroyer getProcessDestroyer() { |
| return this.processDestroyer; |
| } |
| |
| /** |
| * @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer) |
| */ |
| @Override |
| public void setProcessDestroyer(final ProcessDestroyer processDestroyer) { |
| this.processDestroyer = processDestroyer; |
| } |
| |
| /** |
| * @see org.apache.commons.exec.Executor#getWorkingDirectory() |
| */ |
| @Override |
| public File getWorkingDirectory() { |
| return workingDirectory; |
| } |
| |
| /** |
| * @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File) |
| */ |
| @Override |
| public void setWorkingDirectory(final File dir) { |
| this.workingDirectory = dir; |
| } |
| |
| /** |
| * @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, java.util.Map) |
| */ |
| @Override |
| public int execute(final CommandLine command, final Map<String, String> environment) |
| throws ExecuteException, IOException { |
| |
| if (workingDirectory != null && !workingDirectory.exists()) { |
| throw new IOException(workingDirectory + " doesn't exist."); |
| } |
| |
| return executeInternal(command, environment, workingDirectory, streamHandler); |
| |
| } |
| |
| /** |
| * @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, org.apache.commons.exec.ExecuteResultHandler) |
| */ |
| @Override |
| public void execute(final CommandLine command, final Map<String, String> environment, |
| final ExecuteResultHandler handler) throws ExecuteException, IOException { |
| |
| if (workingDirectory != null && !workingDirectory.exists()) { |
| throw new IOException(workingDirectory + " doesn't exist."); |
| } |
| |
| if (watchdog != null) { |
| watchdog.setProcessNotStarted(); |
| } |
| |
| final Runnable runnable = new Runnable() |
| { |
| @Override |
| public void run() |
| { |
| int exitValue = Executor.INVALID_EXITVALUE; |
| try { |
| exitValue = executeInternal(command, environment, workingDirectory, streamHandler); |
| handler.onProcessComplete(exitValue); |
| } catch (final ExecuteException e) { |
| handler.onProcessFailed(e); |
| } catch (final Exception e) { |
| handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e)); |
| } |
| } |
| }; |
| |
| this.executorThread = createThread(runnable, "Exec Default Executor"); |
| getExecutorThread().start(); |
| } |
| |
| /** @see org.apache.commons.exec.Executor#setExitValue(int) */ |
| @Override |
| public void setExitValue(final int value) { |
| this.setExitValues(new int[] {value}); |
| } |
| |
| |
| /** @see org.apache.commons.exec.Executor#setExitValues(int[]) */ |
| @Override |
| public void setExitValues(final int[] values) { |
| this.exitValues = values == null ? null : (int[]) values.clone(); |
| } |
| |
| /** @see org.apache.commons.exec.Executor#isFailure(int) */ |
| @Override |
| public boolean isFailure(final int exitValue) { |
| |
| if (this.exitValues == null) { |
| return false; |
| } |
| else if (this.exitValues.length == 0) { |
| return this.launcher.isFailure(exitValue); |
| } |
| else { |
| for (final int exitValue2 : this.exitValues) { |
| if (exitValue2 == exitValue) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Factory method to create 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 new Thread(runnable, name); |
| } |
| |
| /** |
| * Creates a process that runs a command. |
| * |
| * @param command |
| * the command to run |
| * @param env |
| * the environment for the command |
| * @param dir |
| * 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 dir) throws IOException { |
| |
| if (this.launcher == null) { |
| throw new IllegalStateException("CommandLauncher can not be null"); |
| } |
| |
| if (dir != null && !dir.exists()) { |
| throw new IOException(dir + " doesn't exist."); |
| } |
| return this.launcher.exec(command, env, dir); |
| } |
| |
| /** |
| * Get the worker thread being used for asynchronous execution. |
| * |
| * @return the worker thread |
| */ |
| protected Thread getExecutorThread() { |
| return executorThread; |
| } |
| |
| /** |
| * Close the streams belonging to the given Process. |
| * |
| * @param process the <CODE>Process</CODE>. |
| */ |
| private void closeProcessStreams(final Process process) { |
| |
| try { |
| process.getInputStream().close(); |
| } |
| catch (final IOException e) { |
| setExceptionCaught(e); |
| } |
| |
| try { |
| process.getOutputStream().close(); |
| } |
| catch (final IOException e) { |
| setExceptionCaught(e); |
| } |
| |
| try { |
| process.getErrorStream().close(); |
| } |
| catch (final IOException e) { |
| setExceptionCaught(e); |
| } |
| } |
| |
| /** |
| * 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 dir 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 dir, final ExecuteStreamHandler streams) throws IOException { |
| |
| final Process process; |
| exceptionCaught = null; |
| |
| try { |
| process = this.launch(command, environment, dir); |
| } |
| catch(final IOException e) { |
| if(watchdog != null) { |
| watchdog.failedToStart(e); |
| } |
| throw e; |
| } |
| |
| try { |
| streams.setProcessInputStream(process.getOutputStream()); |
| streams.setProcessOutputStream(process.getInputStream()); |
| streams.setProcessErrorStream(process.getErrorStream()); |
| } 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 (this.getProcessDestroyer() != null) { |
| this.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) { |
| // Java 1.5 does not support public IOException(String message, Throwable cause) |
| final IOException ioe = new IOException(e.getMessage()); |
| ioe.initCause(e); |
| throw ioe; |
| } |
| } |
| |
| if (this.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 (this.getProcessDestroyer() != null) { |
| this.getProcessDestroyer().remove(process); |
| } |
| } |
| } |
| |
| /** |
| * Keep track of the first IOException being thrown. |
| * |
| * @param e the IOException |
| */ |
| private void setExceptionCaught(final IOException e) { |
| if (this.exceptionCaught == null) { |
| this.exceptionCaught = e; |
| } |
| } |
| |
| /** |
| * Get the first IOException being thrown. |
| * |
| * @return the first IOException being caught |
| */ |
| private IOException getExceptionCaught() { |
| return this.exceptionCaught; |
| } |
| |
| } |