001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.exec;
018
019import java.io.File;
020import java.io.IOException;
021import java.util.Map;
022
023import org.apache.commons.exec.launcher.CommandLauncher;
024import org.apache.commons.exec.launcher.CommandLauncherFactory;
025
026/**
027 * The default class to start a subprocess. The implementation
028 * allows to
029 * <ul>
030 *  <li>set a current working directory for the subprocess</li>
031 *  <li>provide a set of environment variables passed to the subprocess</li>
032 *  <li>capture the subprocess output of stdout and stderr using an ExecuteStreamHandler</li>
033 *  <li>kill long-running processes using an ExecuteWatchdog</li>
034 *  <li>define a set of expected exit values</li>
035 *  <li>terminate any started processes when the main process is terminating using a ProcessDestroyer</li>
036 * </ul>
037 *
038 * The following example shows the basic usage:
039 *
040 * <pre>
041 * Executor exec = new DefaultExecutor();
042 * CommandLine cl = new CommandLine("ls -l");
043 * int exitvalue = exec.execute(cl);
044 * </pre>
045 *
046 * @version $Id: DefaultExecutor.java 1557338 2014-01-11 10:34:22Z sebb $
047 */
048public class DefaultExecutor implements Executor {
049
050    /** taking care of output and error stream */
051    private ExecuteStreamHandler streamHandler;
052
053    /** the working directory of the process */
054    private File workingDirectory;
055
056    /** monitoring of long running processes */
057    private ExecuteWatchdog watchdog;
058
059    /** the exit values considered to be successful */
060    private int[] exitValues;
061
062    /** launches the command in a new process */
063    private final CommandLauncher launcher;
064
065    /** optional cleanup of started processes */ 
066    private ProcessDestroyer processDestroyer;
067
068    /** worker thread for asynchronous execution */
069    private Thread executorThread;
070
071    /** the first exception being caught to be thrown to the caller */
072    private IOException exceptionCaught;
073
074    /**
075     * Default constructor creating a default <code>PumpStreamHandler</code>
076     * and sets the working directory of the subprocess to the current
077     * working directory.
078     *
079     * The <code>PumpStreamHandler</code> pumps the output of the subprocess
080     * into our <code>System.out</code> and <code>System.err</code> to avoid
081     * into our <code>System.out</code> and <code>System.err</code> to avoid
082     * a blocked or deadlocked subprocess (see{@link java.lang.Process Process}).
083     */
084    public DefaultExecutor() {
085        this.streamHandler = new PumpStreamHandler();
086        this.launcher = CommandLauncherFactory.createVMLauncher();
087        this.exitValues = new int[0];
088        this.workingDirectory = new File(".");
089        this.exceptionCaught = null;
090    }
091
092    /**
093     * @see org.apache.commons.exec.Executor#getStreamHandler()
094     */
095    public ExecuteStreamHandler getStreamHandler() {
096        return streamHandler;
097    }
098
099    /**
100     * @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ExecuteStreamHandler)
101     */
102    public void setStreamHandler(final ExecuteStreamHandler streamHandler) {
103        this.streamHandler = streamHandler;
104    }
105
106    /**
107     * @see org.apache.commons.exec.Executor#getWatchdog()
108     */
109    public ExecuteWatchdog getWatchdog() {
110        return watchdog;
111    }
112
113    /**
114     * @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog)
115     */
116    public void setWatchdog(final ExecuteWatchdog watchDog) {
117        this.watchdog = watchDog;
118    }
119
120    /**
121     * @see org.apache.commons.exec.Executor#getProcessDestroyer()
122     */
123    public ProcessDestroyer getProcessDestroyer() {
124      return this.processDestroyer;
125    }
126
127    /**
128     * @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer)
129     */
130    public void setProcessDestroyer(final ProcessDestroyer processDestroyer) {
131      this.processDestroyer = processDestroyer;
132    }
133
134    /**
135     * @see org.apache.commons.exec.Executor#getWorkingDirectory()
136     */
137    public File getWorkingDirectory() {
138        return workingDirectory;
139    }
140
141    /**
142     * @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File)
143     */
144    public void setWorkingDirectory(final File dir) {
145        this.workingDirectory = dir;
146    }
147
148    /**
149     * @see org.apache.commons.exec.Executor#execute(CommandLine)
150     */
151    public int execute(final CommandLine command) throws ExecuteException,
152            IOException {
153        return execute(command, (Map<String, String>) null);
154    }
155
156    /**
157     * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map)
158     */
159    public int execute(final CommandLine command, final Map<String, String> environment)
160            throws ExecuteException, IOException {
161
162        if (workingDirectory != null && !workingDirectory.exists()) {
163            throw new IOException(workingDirectory + " doesn't exist.");
164        }
165        
166        return executeInternal(command, environment, workingDirectory, streamHandler);
167
168    }
169
170    /**
171     * @see org.apache.commons.exec.Executor#execute(CommandLine,
172     *      org.apache.commons.exec.ExecuteResultHandler)
173     */
174    public void execute(final CommandLine command, final ExecuteResultHandler handler)
175            throws ExecuteException, IOException {
176        execute(command, null, handler);
177    }
178
179    /**
180     * @see org.apache.commons.exec.Executor#execute(CommandLine,
181     *      java.util.Map, org.apache.commons.exec.ExecuteResultHandler)
182     */
183    public void execute(final CommandLine command, final Map<String, String> environment,
184            final ExecuteResultHandler handler) throws ExecuteException, IOException {
185
186        if (workingDirectory != null && !workingDirectory.exists()) {
187            throw new IOException(workingDirectory + " doesn't exist.");
188        }
189
190        if (watchdog != null) {
191            watchdog.setProcessNotStarted();
192        }
193
194        final Runnable runnable = new Runnable()
195        {
196            public void run()
197            {
198                int exitValue = Executor.INVALID_EXITVALUE;
199                try {
200                    exitValue = executeInternal(command, environment, workingDirectory, streamHandler);
201                    handler.onProcessComplete(exitValue);
202                } catch (final ExecuteException e) {
203                    handler.onProcessFailed(e);
204                } catch (final Exception e) {
205                    handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e));
206                }
207            }
208        };
209
210        this.executorThread = createThread(runnable, "Exec Default Executor");
211        getExecutorThread().start();
212    }
213
214    /** @see org.apache.commons.exec.Executor#setExitValue(int) */
215    public void setExitValue(final int value) {
216        this.setExitValues(new int[] {value});
217    }
218
219
220    /** @see org.apache.commons.exec.Executor#setExitValues(int[]) */
221    public void setExitValues(final int[] values) {
222        this.exitValues = values == null ? null : (int[]) values.clone();
223    }
224
225    /** @see org.apache.commons.exec.Executor#isFailure(int) */
226    public boolean isFailure(final int exitValue) {
227
228        if (this.exitValues == null) {
229            return false;
230        }
231        else if (this.exitValues.length == 0) {
232            return this.launcher.isFailure(exitValue);
233        }
234        else {
235            for (int exitValue2 : this.exitValues) {
236                if (exitValue2 == exitValue) {
237                    return false;
238                }
239            }
240        }
241        return true;
242    }
243
244    /**
245     * Factory method to create a thread waiting for the result of an
246     * asynchronous execution.
247     *
248     * @param runnable the runnable passed to the thread
249     * @param name the name of the thread
250     * @return the thread
251     */
252    protected Thread createThread(final Runnable runnable, final String name) {
253        return new Thread(runnable, name);
254    }
255
256    /**
257     * Creates a process that runs a command.
258     *
259     * @param command
260     *            the command to run
261     * @param env
262     *            the environment for the command
263     * @param dir
264     *            the working directory for the command
265     * @return the process started
266     * @throws IOException
267     *             forwarded from the particular launcher used
268     */
269    protected Process launch(final CommandLine command, final Map<String, String> env,
270            final File dir) throws IOException {
271
272        if (this.launcher == null) {
273            throw new IllegalStateException("CommandLauncher can not be null");
274        }
275
276        if (dir != null && !dir.exists()) {
277            throw new IOException(dir + " doesn't exist.");
278        }
279        return this.launcher.exec(command, env, dir);
280    }
281
282    /**
283     * Get the worker thread being used for asynchronous execution.
284     *
285     * @return the worker thread
286     */
287    protected Thread getExecutorThread() {
288        return executorThread;
289    }
290    
291    /**
292     * Close the streams belonging to the given Process.
293     *
294     * @param process the <CODE>Process</CODE>.
295     */
296    private void closeProcessStreams(final Process process) {
297
298        try {
299            process.getInputStream().close();
300        }
301        catch (final IOException e) {
302            setExceptionCaught(e);
303        }
304
305        try {
306            process.getOutputStream().close();
307        }
308        catch (final IOException e) {
309            setExceptionCaught(e);
310        }
311
312        try {
313            process.getErrorStream().close();
314        }
315        catch (final IOException e) {
316            setExceptionCaught(e);
317        }
318    }
319
320    /**
321     * Execute an internal process. If the executing thread is interrupted while waiting for the
322     * child process to return the child process will be killed.
323     *
324     * @param command the command to execute
325     * @param environment the execution environment
326     * @param dir the working directory
327     * @param streams process the streams (in, out, err) of the process
328     * @return the exit code of the process
329     * @throws IOException executing the process failed
330     */
331    private int executeInternal(final CommandLine command, final Map<String, String> environment,
332            final File dir, final ExecuteStreamHandler streams) throws IOException {
333
334        setExceptionCaught(null);
335
336        final Process process = this.launch(command, environment, dir);
337
338        try {
339            streams.setProcessInputStream(process.getOutputStream());
340            streams.setProcessOutputStream(process.getInputStream());
341            streams.setProcessErrorStream(process.getErrorStream());
342        } catch (final IOException e) {
343            process.destroy();
344            throw e;
345        }
346
347        streams.start();
348
349        try {
350
351            // add the process to the list of those to destroy if the VM exits
352            if (this.getProcessDestroyer() != null) {
353              this.getProcessDestroyer().add(process);
354            }
355
356            // associate the watchdog with the newly created process
357            if (watchdog != null) {
358                watchdog.start(process);
359            }
360
361            int exitValue = Executor.INVALID_EXITVALUE;
362
363            try {
364                exitValue = process.waitFor();
365            } catch (final InterruptedException e) {
366                process.destroy();
367            }
368            finally {
369                // see http://bugs.sun.com/view_bug.do?bug_id=6420270
370                // see https://issues.apache.org/jira/browse/EXEC-46
371                // Process.waitFor should clear interrupt status when throwing InterruptedException
372                // but we have to do that manually
373                Thread.interrupted();
374            }            
375
376            if (watchdog != null) {
377                watchdog.stop();
378            }
379
380            try {
381                streams.stop();
382            }
383            catch (final IOException e) {
384                setExceptionCaught(e);
385            }
386
387            closeProcessStreams(process);
388
389            if (getExceptionCaught() != null) {
390                throw getExceptionCaught();
391            }
392
393            if (watchdog != null) {
394                try {
395                    watchdog.checkException();
396                } catch (final IOException e) {
397                    throw e;
398                } catch (final Exception e) {
399                    throw new IOException(e.getMessage());
400                }
401            }
402
403            if (this.isFailure(exitValue)) {
404                throw new ExecuteException("Process exited with an error: " + exitValue, exitValue);
405            }
406
407            return exitValue;
408        } finally {
409            // remove the process to the list of those to destroy if the VM exits
410            if (this.getProcessDestroyer() != null) {
411              this.getProcessDestroyer().remove(process);
412            }
413        }
414    }
415
416    /**
417     * Keep track of the first IOException being thrown.
418     *
419     * @param e the IOException
420     */
421    private void setExceptionCaught(final IOException e) {
422        if (this.exceptionCaught == null) {
423            this.exceptionCaught = e;
424        }
425    }
426
427    /**
428     * Get the first IOException being thrown.
429     *
430     * @return the first IOException being caught
431     */
432    private IOException getExceptionCaught() {
433        return this.exceptionCaught;
434    }
435
436}