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     */
017    package org.apache.commons.exec;
018    
019    import java.io.File;
020    import java.io.IOException;
021    import java.util.Map;
022    
023    import org.apache.commons.exec.launcher.CommandLauncher;
024    import 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    public 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        /**
070         * Default constructor creating a default <code>PumpStreamHandler</code>
071         * and sets the working directory of the subprocess to the current
072         * working directory.
073         *
074         * The <code>PumpStreamHandler</code> pumps the output of the subprocess
075         * into our <code>System.out</code> and <code>System.err</code> to avoid
076         * into our <code>System.out</code> and <code>System.err</code> to avoid
077         * a blocked or deadlocked subprocess (see{@link java.lang.Process Process}).
078         */
079        public DefaultExecutor() {
080            this.streamHandler = new PumpStreamHandler();
081            this.launcher = CommandLauncherFactory.createVMLauncher();
082            this.exitValues = new int[0];
083            this.workingDirectory = new File(".");
084        }
085    
086        /**
087         * @see org.apache.commons.exec.Executor#getStreamHandler()
088         */
089        public ExecuteStreamHandler getStreamHandler() {
090            return streamHandler;
091        }
092    
093        /**
094         * @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ExecuteStreamHandler)
095         */
096        public void setStreamHandler(ExecuteStreamHandler streamHandler) {
097            this.streamHandler = streamHandler;
098        }
099    
100        /**
101         * @see org.apache.commons.exec.Executor#getWatchdog()
102         */
103        public ExecuteWatchdog getWatchdog() {
104            return watchdog;
105        }
106    
107        /**
108         * @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog)
109         */
110        public void setWatchdog(ExecuteWatchdog watchDog) {
111            this.watchdog = watchDog;
112        }
113    
114        /**
115         * @see org.apache.commons.exec.Executor#getProcessDestroyer()
116         */
117        public ProcessDestroyer getProcessDestroyer() {
118          return this.processDestroyer;
119        }
120    
121        /**
122         * @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer)
123         */
124        public void setProcessDestroyer(ProcessDestroyer processDestroyer) {
125          this.processDestroyer = processDestroyer;
126        }
127    
128        /**
129         * @see org.apache.commons.exec.Executor#getWorkingDirectory()
130         */
131        public File getWorkingDirectory() {
132            return workingDirectory;
133        }
134    
135        /**
136         * @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File)
137         */
138        public void setWorkingDirectory(File dir) {
139            this.workingDirectory = dir;
140        }
141    
142        /**
143         * @see org.apache.commons.exec.Executor#execute(CommandLine)
144         */
145        public int execute(final CommandLine command) throws ExecuteException,
146                IOException {
147            return execute(command, (Map) null);
148        }
149    
150        /**
151         * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map)
152         */
153        public int execute(final CommandLine command, Map environment)
154                throws ExecuteException, IOException {
155    
156            if (workingDirectory != null && !workingDirectory.exists()) {
157                throw new IOException(workingDirectory + " doesn't exist.");
158            }
159            
160            return executeInternal(command, environment, workingDirectory, streamHandler);
161    
162        }
163    
164        /**
165         * @see org.apache.commons.exec.Executor#execute(CommandLine,
166         *      org.apache.commons.exec.ExecuteResultHandler)
167         */
168        public void execute(final CommandLine command, ExecuteResultHandler handler)
169                throws ExecuteException, IOException {
170            execute(command, null, handler);
171        }
172    
173        /**
174         * @see org.apache.commons.exec.Executor#execute(CommandLine,
175         *      java.util.Map, org.apache.commons.exec.ExecuteResultHandler)
176         */
177        public void execute(final CommandLine command, final Map environment,
178                final ExecuteResultHandler handler) throws ExecuteException, IOException {
179    
180            if (workingDirectory != null && !workingDirectory.exists()) {
181                throw new IOException(workingDirectory + " doesn't exist.");
182            }
183    
184            executorThread = new Thread() {
185                public void run() {
186                    int exitValue = Executor.INVALID_EXITVALUE;
187                    try {                    
188                        exitValue = executeInternal(command, environment, workingDirectory, streamHandler);
189                        handler.onProcessComplete(exitValue);
190                    } catch (ExecuteException e) {
191                        handler.onProcessFailed(e);
192                    } catch(Exception e) {
193                        handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e));
194                    }
195                }
196            };
197    
198            getExecutorThread().start();
199        }
200    
201        /** @see org.apache.commons.exec.Executor#setExitValue(int) */
202        public void setExitValue(final int value) {
203            this.setExitValues(new int[] {value});
204        }
205    
206    
207        /** @see org.apache.commons.exec.Executor#setExitValues(int[]) */
208        public void setExitValues(final int[] values) {
209            this.exitValues = (values == null ? null : (int[]) values.clone());
210        }
211    
212        /** @see org.apache.commons.exec.Executor#isFailure(int) */
213        public boolean isFailure(final int exitValue) {
214    
215            if(this.exitValues == null) {
216                return false;
217            }
218            else if(this.exitValues.length == 0) {
219                return this.launcher.isFailure(exitValue);
220            }
221            else {
222                for(int i=0; i<this.exitValues.length; i++) {
223                    if(this.exitValues[i] == exitValue) {
224                        return false;
225                    }
226                }
227            }
228            return true;
229        }
230    
231        /**
232         * Creates a process that runs a command.
233         *
234         * @param command
235         *            the command to run
236         * @param env
237         *            the environment for the command
238         * @param dir
239         *            the working directory for the command
240         * @return the process started
241         * @throws IOException
242         *             forwarded from the particular launcher used
243         */
244        protected Process launch(final CommandLine command, final Map env,
245                final File dir) throws IOException {
246    
247            if (this.launcher == null) {
248                throw new IllegalStateException("CommandLauncher can not be null");
249            }
250    
251            if (dir != null && !dir.exists()) {
252                throw new IOException(dir + " doesn't exist.");
253            }
254            return this.launcher.exec(command, env, dir);
255        }
256    
257        /**
258         * Get the worker thread being used for asynchronous execution.
259         *
260         * @return the worker thread
261         */
262        protected Thread getExecutorThread() {
263            return executorThread;
264        }
265        
266        /**
267         * Close the streams belonging to the given Process. In the
268         * original implementation all exceptions were dropped which
269         * is probably not a good thing. On the other hand the signature
270         * allows throwing an IOException so the current implementation
271         * might be quite okay.
272         * 
273         * @param process the <CODE>Process</CODE>.
274         * @throws IOException closing one of the three streams failed
275         */
276        private void closeStreams(final Process process) throws IOException {
277    
278            IOException caught = null;
279    
280            try {
281                process.getInputStream().close();
282            }
283            catch(IOException e) {
284                caught = e;
285            }
286    
287            try {
288                process.getOutputStream().close();
289            }
290            catch(IOException e) {
291                caught = e;
292            }
293    
294            try {
295                process.getErrorStream().close();
296            }
297            catch(IOException e) {
298                caught = e;
299            }
300    
301            if(caught != null) {
302                throw caught;
303            }
304        }
305    
306        /**
307         * Execute an internal process.
308         *
309         * @param command the command to execute
310         * @param environment the execution enviroment
311         * @param dir the working directory
312         * @param streams process the streams (in, out, err) of the process
313         * @return the exit code of the process
314         * @throws IOException executing the process failed
315         */
316        private int executeInternal(final CommandLine command, final Map environment,
317                final File dir, final ExecuteStreamHandler streams) throws IOException {
318    
319            final Process process = this.launch(command, environment, dir);
320    
321            try {
322                streams.setProcessInputStream(process.getOutputStream());
323                streams.setProcessOutputStream(process.getInputStream());
324                streams.setProcessErrorStream(process.getErrorStream());
325            } catch (IOException e) {
326                process.destroy();
327                throw e;
328            }
329    
330            streams.start();
331    
332            try {
333    
334                // add the process to the list of those to destroy if the VM exits
335                if(this.getProcessDestroyer() != null) {
336                  this.getProcessDestroyer().add(process);
337                }
338    
339                // associate the watchdog with the newly created process
340                if (watchdog != null) {
341                    watchdog.start(process);
342                }
343    
344                int exitValue = Executor.INVALID_EXITVALUE;
345    
346                try {
347                    exitValue = process.waitFor();
348                } catch (InterruptedException e) {
349                    process.destroy();
350                }
351                finally {
352                    // see http://bugs.sun.com/view_bug.do?bug_id=6420270
353                    // see https://issues.apache.org/jira/browse/EXEC-46
354                    // Process.waitFor should clear interrupt status when throwing InterruptedException
355                    // but we have to do that manually
356                    Thread.interrupted();
357                }            
358    
359                if (watchdog != null) {
360                    watchdog.stop();
361                }
362    
363                streams.stop();
364                closeStreams(process);
365    
366                if (watchdog != null) {
367                    try {
368                        watchdog.checkException();
369                    } catch (IOException e) {
370                        throw e;
371                    } catch (Exception e) {
372                        throw new IOException(e.getMessage());
373                    }
374                }
375    
376                if(this.isFailure(exitValue)) {
377                    throw new ExecuteException("Process exited with an error: " + exitValue, exitValue);
378                }
379    
380                return exitValue;
381            } finally {
382                // remove the process to the list of those to destroy if the VM exits
383                if(this.getProcessDestroyer() != null) {
384                  this.getProcessDestroyer().remove(process);
385                }
386            }
387        }
388    }