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