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