View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.exec;
18  
19  import java.io.Closeable;
20  import java.io.File;
21  import java.io.IOException;
22  import java.util.Map;
23  import java.util.concurrent.Executors;
24  import java.util.concurrent.ThreadFactory;
25  import java.util.function.Supplier;
26  
27  import org.apache.commons.exec.launcher.CommandLauncher;
28  import org.apache.commons.exec.launcher.CommandLauncherFactory;
29  
30  /**
31   * The default class to start a subprocess. The implementation allows to
32   * <ul>
33   * <li>set a current working directory for the subprocess</li>
34   * <li>provide a set of environment variables passed to the subprocess</li>
35   * <li>capture the subprocess output of stdout and stderr using an ExecuteStreamHandler</li>
36   * <li>kill long-running processes using an ExecuteWatchdog</li>
37   * <li>define a set of expected exit values</li>
38   * <li>terminate any started processes when the main process is terminating using a ProcessDestroyer</li>
39   * </ul>
40   *
41   * The following example shows the basic usage:
42   *
43   * <pre>
44   * Executor exec = DefaultExecutor.builder().get();
45   * CommandLine cl = new CommandLine("ls -l");
46   * int exitvalue = exec.execute(cl);
47   * </pre>
48   */
49  public class DefaultExecutor implements Executor {
50  
51      /**
52       * Constructs a new builder.
53       *
54       * @param <T> The builder type.
55       * @since 1.4.0
56       */
57      public static class Builder<T extends Builder<T>> implements Supplier<DefaultExecutor> {
58  
59          private ThreadFactory threadFactory;
60          private ExecuteStreamHandler executeStreamHandler;
61          private File workingDirectory;
62  
63          @SuppressWarnings("unchecked")
64          T asThis() {
65              return (T) this;
66          }
67  
68          /**
69           * Creates a new configured DefaultExecutor.
70           *
71           * @return a new configured DefaultExecutor.
72           */
73          @Override
74          public DefaultExecutor get() {
75              return new DefaultExecutor(threadFactory, executeStreamHandler, workingDirectory);
76          }
77  
78          ExecuteStreamHandler getExecuteStreamHandler() {
79              return executeStreamHandler;
80          }
81  
82          ThreadFactory getThreadFactory() {
83              return threadFactory;
84          }
85  
86          File getWorkingDirectory() {
87              return workingDirectory;
88          }
89  
90          /**
91           * Sets the PumpStreamHandler.
92           *
93           * @param executeStreamHandler the ExecuteStreamHandler, null resets to the default.
94           * @return this.
95           */
96          public T setExecuteStreamHandler(final ExecuteStreamHandler executeStreamHandler) {
97              this.executeStreamHandler = executeStreamHandler;
98              return asThis();
99          }
100 
101         /**
102          * Sets the ThreadFactory.
103          *
104          * @param threadFactory the ThreadFactory, null resets to the default.
105          * @return this.
106          */
107         public T setThreadFactory(final ThreadFactory threadFactory) {
108             this.threadFactory = threadFactory;
109             return asThis();
110         }
111 
112         /**
113          * Sets the working directory.
114          *
115          * @param workingDirectory the working directory., null resets to the default.
116          * @return this.
117          */
118         public T setWorkingDirectory(final File workingDirectory) {
119             this.workingDirectory = workingDirectory;
120             return asThis();
121         }
122 
123     }
124 
125     /**
126      * Creates a new builder.
127      *
128      * @return a new builder.
129      * @since 1.4.0
130      */
131     public static Builder<?> builder() {
132         return new Builder<>();
133     }
134 
135     /** Taking care of output and error stream. */
136     private ExecuteStreamHandler executeStreamHandler;
137 
138     /** The working directory of the process. */
139     private File workingDirectory;
140 
141     /** Monitoring of long running processes. */
142     private ExecuteWatchdog watchdog;
143 
144     /** The exit values considered to be successful. */
145     private int[] exitValues;
146 
147     /** Launches the command in a new process. */
148     private final CommandLauncher launcher;
149 
150     /** Optional cleanup of started processes. */
151     private ProcessDestroyer processDestroyer;
152 
153     /** Worker thread for asynchronous execution. */
154     private Thread executorThread;
155 
156     /** The first exception being caught to be thrown to the caller. */
157     private IOException exceptionCaught;
158 
159     /**
160      * The thread factory.
161      */
162     private final ThreadFactory threadFactory;
163 
164     /**
165      * Constructs a default {@code PumpStreamHandler} and sets the working directory of the subprocess to the current working directory.
166      *
167      * 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}
168      * and {@code System.err} to avoid a blocked or deadlocked subprocess (see {@link Process Process}).
169      *
170      * @deprecated Use {@link Builder#get()}.
171      */
172     @Deprecated
173     public DefaultExecutor() {
174         this(Executors.defaultThreadFactory(), new PumpStreamHandler(), new File("."));
175     }
176 
177     DefaultExecutor(final ThreadFactory threadFactory, final ExecuteStreamHandler executeStreamHandler, final File workingDirectory) {
178         this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory();
179         this.executeStreamHandler = executeStreamHandler != null ? executeStreamHandler : new PumpStreamHandler();
180         this.workingDirectory = workingDirectory != null ? workingDirectory : new File(".");
181         this.launcher = CommandLauncherFactory.createVMLauncher();
182         this.exitValues = new int[0];
183     }
184 
185     private void checkWorkingDirectory() throws IOException {
186         checkWorkingDirectory(workingDirectory);
187     }
188 
189     private void checkWorkingDirectory(final File directory) throws IOException {
190         if (directory != null && !directory.exists()) {
191             throw new IOException(directory + " doesn't exist.");
192         }
193     }
194 
195     /**
196      * Closes the Closeable, remembering any exception.
197      *
198      * @param closeable the {@link Closeable} to close.
199      */
200     private void closeCatch(final Closeable closeable) {
201         try {
202             closeable.close();
203         } catch (final IOException e) {
204             setExceptionCaught(e);
205         }
206     }
207 
208     /**
209      * Closes the streams belonging to the given Process.
210      *
211      * @param process the {@link Process}.
212      */
213     @SuppressWarnings("resource")
214     private void closeProcessStreams(final Process process) {
215         closeCatch(process.getInputStream());
216         closeCatch(process.getOutputStream());
217         closeCatch(process.getErrorStream());
218     }
219 
220     /**
221      * Creates a thread waiting for the result of an asynchronous execution.
222      *
223      * @param runnable the runnable passed to the thread.
224      * @param name     the name of the thread.
225      * @return the thread
226      */
227     protected Thread createThread(final Runnable runnable, final String name) {
228         return ThreadUtil.newThread(threadFactory, runnable, name, false);
229     }
230 
231     /**
232      * @see org.apache.commons.exec.Executor#execute(CommandLine)
233      */
234     @Override
235     public int execute(final CommandLine command) throws ExecuteException, IOException {
236         return execute(command, (Map<String, String>) null);
237     }
238 
239     /**
240      * @see org.apache.commons.exec.Executor#execute(CommandLine, org.apache.commons.exec.ExecuteResultHandler)
241      */
242     @Override
243     public void execute(final CommandLine command, final ExecuteResultHandler handler) throws ExecuteException, IOException {
244         execute(command, null, handler);
245     }
246 
247     /**
248      * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map)
249      */
250     @Override
251     public int execute(final CommandLine command, final Map<String, String> environment) throws ExecuteException, IOException {
252         checkWorkingDirectory();
253         return executeInternal(command, environment, workingDirectory, executeStreamHandler);
254     }
255 
256     /**
257      * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map, org.apache.commons.exec.ExecuteResultHandler)
258      */
259     @Override
260     public void execute(final CommandLine command, final Map<String, String> environment, final ExecuteResultHandler handler)
261             throws ExecuteException, IOException {
262         checkWorkingDirectory();
263         if (watchdog != null) {
264             watchdog.setProcessNotStarted();
265         }
266         executorThread = createThread(() -> {
267             int exitValue = Executor.INVALID_EXITVALUE;
268             try {
269                 exitValue = executeInternal(command, environment, workingDirectory, executeStreamHandler);
270                 handler.onProcessComplete(exitValue);
271             } catch (final ExecuteException e) {
272                 handler.onProcessFailed(e);
273             } catch (final Exception e) {
274                 handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e));
275             }
276         }, "CommonsExecDefaultExecutor");
277         getExecutorThread().start();
278     }
279 
280     /**
281      * Execute an internal process. If the executing thread is interrupted while waiting for the child process to return the child process will be killed.
282      *
283      * @param command          the command to execute.
284      * @param environment      the execution environment.
285      * @param workingDirectory the working directory.
286      * @param streams          process the streams (in, out, err) of the process.
287      * @return the exit code of the process.
288      * @throws IOException executing the process failed.
289      */
290     private int executeInternal(final CommandLine command, final Map<String, String> environment, final File workingDirectory,
291             final ExecuteStreamHandler streams) throws IOException {
292         final Process process;
293         exceptionCaught = null;
294         try {
295             process = launch(command, environment, workingDirectory);
296         } catch (final IOException e) {
297             if (watchdog != null) {
298                 watchdog.failedToStart(e);
299             }
300             throw e;
301         }
302         try {
303             setStreams(streams, process);
304         } catch (final IOException e) {
305             process.destroy();
306             if (watchdog != null) {
307                 watchdog.failedToStart(e);
308             }
309             throw e;
310         }
311         streams.start();
312         try {
313             // add the process to the list of those to destroy if the VM exits
314             if (getProcessDestroyer() != null) {
315                 getProcessDestroyer().add(process);
316             }
317             // associate the watchdog with the newly created process
318             if (watchdog != null) {
319                 watchdog.start(process);
320             }
321             int exitValue = Executor.INVALID_EXITVALUE;
322             try {
323                 exitValue = process.waitFor();
324             } catch (final InterruptedException e) {
325                 process.destroy();
326             } finally {
327                 // see http://bugs.sun.com/view_bug.do?bug_id=6420270
328                 // see https://issues.apache.org/jira/browse/EXEC-46
329                 // Process.waitFor should clear interrupt status when throwing InterruptedException
330                 // but we have to do that manually
331                 Thread.interrupted();
332             }
333             if (watchdog != null) {
334                 watchdog.stop();
335             }
336             try {
337                 streams.stop();
338             } catch (final IOException e) {
339                 setExceptionCaught(e);
340             }
341             closeProcessStreams(process);
342             if (getExceptionCaught() != null) {
343                 throw getExceptionCaught();
344             }
345             if (watchdog != null) {
346                 try {
347                     watchdog.checkException();
348                 } catch (final IOException e) {
349                     throw e;
350                 } catch (final Exception e) {
351                     throw new IOException(e);
352                 }
353             }
354             if (isFailure(exitValue)) {
355                 throw new ExecuteException("Process exited with an error: " + exitValue, exitValue);
356             }
357             return exitValue;
358         } finally {
359             // remove the process to the list of those to destroy if the VM exits
360             if (getProcessDestroyer() != null) {
361                 getProcessDestroyer().remove(process);
362             }
363         }
364     }
365 
366     /**
367      * Gets the first IOException being thrown.
368      *
369      * @return the first IOException being caught.
370      */
371     private IOException getExceptionCaught() {
372         return exceptionCaught;
373     }
374 
375     /**
376      * Gets the worker thread being used for asynchronous execution.
377      *
378      * @return the worker thread.
379      */
380     protected Thread getExecutorThread() {
381         return executorThread;
382     }
383 
384     /**
385      * @see org.apache.commons.exec.Executor#getProcessDestroyer()
386      */
387     @Override
388     public ProcessDestroyer getProcessDestroyer() {
389         return processDestroyer;
390     }
391 
392     /**
393      * @see org.apache.commons.exec.Executor#getStreamHandler()
394      */
395     @Override
396     public ExecuteStreamHandler getStreamHandler() {
397         return executeStreamHandler;
398     }
399 
400     /**
401      * Gets the thread factory. Z
402      *
403      * @return the thread factory.
404      */
405     ThreadFactory getThreadFactory() {
406         return threadFactory;
407     }
408 
409     /**
410      * @see org.apache.commons.exec.Executor#getWatchdog()
411      */
412     @Override
413     public ExecuteWatchdog getWatchdog() {
414         return watchdog;
415     }
416 
417     /**
418      * @see org.apache.commons.exec.Executor#getWorkingDirectory()
419      */
420     @Override
421     public File getWorkingDirectory() {
422         return workingDirectory;
423     }
424 
425     /** @see org.apache.commons.exec.Executor#isFailure(int) */
426     @Override
427     public boolean isFailure(final int exitValue) {
428         if (exitValues == null) {
429             return false;
430         }
431         if (exitValues.length == 0) {
432             return launcher.isFailure(exitValue);
433         }
434         for (final int exitValue2 : exitValues) {
435             if (exitValue2 == exitValue) {
436                 return false;
437             }
438         }
439         return true;
440     }
441 
442     /**
443      * Creates a process that runs a command.
444      *
445      * @param command          the command to run.
446      * @param env              the environment for the command.
447      * @param workingDirectory the working directory for the command.
448      * @return the process started.
449      * @throws IOException forwarded from the particular launcher used.
450      */
451     protected Process launch(final CommandLine command, final Map<String, String> env, final File workingDirectory) throws IOException {
452         if (launcher == null) {
453             throw new IllegalStateException("CommandLauncher can not be null");
454         }
455         checkWorkingDirectory(workingDirectory);
456         return launcher.exec(command, env, workingDirectory);
457     }
458 
459     /**
460      * Sets the first IOException thrown.
461      *
462      * @param e the IOException.
463      */
464     private void setExceptionCaught(final IOException e) {
465         if (exceptionCaught == null) {
466             exceptionCaught = e;
467         }
468     }
469 
470     /** @see org.apache.commons.exec.Executor#setExitValue(int) */
471     @Override
472     public void setExitValue(final int value) {
473         setExitValues(new int[] { value });
474     }
475 
476     /** @see org.apache.commons.exec.Executor#setExitValues(int[]) */
477     @Override
478     public void setExitValues(final int[] values) {
479         exitValues = values == null ? null : (int[]) values.clone();
480     }
481 
482     /**
483      * @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer)
484      */
485     @Override
486     public void setProcessDestroyer(final ProcessDestroyer processDestroyer) {
487         this.processDestroyer = processDestroyer;
488     }
489 
490     /**
491      * @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ExecuteStreamHandler)
492      */
493     @Override
494     public void setStreamHandler(final ExecuteStreamHandler streamHandler) {
495         this.executeStreamHandler = streamHandler;
496     }
497 
498     @SuppressWarnings("resource")
499     private void setStreams(final ExecuteStreamHandler streams, final Process process) throws IOException {
500         streams.setProcessInputStream(process.getOutputStream());
501         streams.setProcessOutputStream(process.getInputStream());
502         streams.setProcessErrorStream(process.getErrorStream());
503     }
504 
505     /**
506      * @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog)
507      */
508     @Override
509     public void setWatchdog(final ExecuteWatchdog watchdog) {
510         this.watchdog = watchdog;
511     }
512 
513     /**
514      * Sets the working directory.
515      *
516      * @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File)
517      * @deprecated Use {@link Builder#setWorkingDirectory(File)}.
518      */
519     @Deprecated
520     @Override
521     public void setWorkingDirectory(final File workingDirectory) {
522         this.workingDirectory = workingDirectory;
523     }
524 
525 }