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