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  
18  package org.apache.commons.exec;
19  
20  import java.time.Duration;
21  import java.util.Objects;
22  import java.util.concurrent.Executors;
23  import java.util.concurrent.ThreadFactory;
24  import java.util.function.Supplier;
25  
26  import org.apache.commons.exec.util.DebugUtils;
27  
28  /**
29   * Destroys a process running for too long. For example:
30   *
31   * <pre>
32   * ExecuteWatchdog watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofSeconds(30)).get();
33   * Executor executor = DefaultExecutor.builder().setExecuteStreamHandler(new PumpStreamHandler()).get();
34   * executor.setWatchdog(watchdog);
35   * int exitValue = executor.execute(myCommandLine);
36   * if (executor.isFailure(exitValue) &amp;&amp; watchdog.killedProcess()) {
37   *     // it was killed on purpose by the watchdog
38   * }
39   * </pre>
40   * <p>
41   * When starting an asynchronous process than 'ExecuteWatchdog' is the keeper of the process handle. In some cases it is useful not to define a timeout (and
42   * pass {@link #INFINITE_TIMEOUT_DURATION}) and to kill the process explicitly using {@link #destroyProcess()}.
43   * </p>
44   * <p>
45   * Please note that ExecuteWatchdog is processed asynchronously, e.g. it might be still attached to a process even after the
46   * {@link DefaultExecutor#execute(CommandLine)} or a variation has returned.
47   * </p>
48   *
49   * @see Executor
50   * @see Watchdog
51   */
52  public class ExecuteWatchdog implements TimeoutObserver {
53  
54      /**
55       * Builds ExecuteWatchdog instances.
56       *
57       * @since 1.4.0
58       */
59      public static final class Builder implements Supplier<ExecuteWatchdog> {
60  
61          private ThreadFactory threadFactory;
62          private Duration timeout;
63  
64          /**
65           * Creates a new configured ExecuteWatchdog.
66           *
67           * @return a new configured ExecuteWatchdog.
68           */
69          @Override
70          public ExecuteWatchdog get() {
71              return new ExecuteWatchdog(threadFactory, timeout);
72          }
73  
74          /**
75           * Sets the thread factory.
76           *
77           * @param threadFactory the thread factory.
78           * @return this.
79           */
80          public Builder setThreadFactory(final ThreadFactory threadFactory) {
81              this.threadFactory = threadFactory;
82              return this;
83          }
84  
85          /**
86           * Sets the timeout duration.
87           *
88           * @param timeout the timeout duration.
89           * @return this.
90           */
91          public Builder setTimeout(final Duration timeout) {
92              this.timeout = timeout;
93              return this;
94          }
95  
96      }
97  
98      /** The marker for an infinite timeout. */
99      public static final long INFINITE_TIMEOUT = -1;
100 
101     /** The marker for an infinite timeout. */
102     public static final Duration INFINITE_TIMEOUT_DURATION = Duration.ofMillis(INFINITE_TIMEOUT);
103 
104     /**
105      * Creates a new builder.
106      *
107      * @return a new builder.
108      * @since 1.4.0
109      */
110     public static Builder builder() {
111         return new Builder();
112     }
113 
114     /** The process to execute and watch for duration. */
115     private Process process;
116 
117     /** Is a user-supplied timeout in use. */
118     private final boolean hasWatchdog;
119 
120     /** Say whether or not the watchdog is currently monitoring a process. */
121     private boolean watch;
122 
123     /** Exception that might be thrown during the process execution. */
124     private Exception caught;
125 
126     /** Say whether or not the process was killed due to running overtime. */
127     private boolean killedProcess;
128 
129     /** Will tell us whether timeout has occurred. */
130     private final Watchdog watchdog;
131 
132     /** Indicates that the process is verified as started */
133     private volatile boolean processStarted;
134 
135     /**
136      * The thread factory.
137      */
138     private final ThreadFactory threadFactory;
139 
140     /**
141      * Creates a new watchdog with a given timeout.
142      *
143      * @param timeoutMillis the timeout for the process in milliseconds. It must be greater than 0 or {@code INFINITE_TIMEOUT}.
144      * @deprecated Use {@link Builder#get()}.
145      */
146     @Deprecated
147     public ExecuteWatchdog(final long timeoutMillis) {
148         this(Executors.defaultThreadFactory(), Duration.ofMillis(timeoutMillis));
149     }
150 
151     /**
152      * Creates a new watchdog with a given timeout.
153      *
154      * @param threadFactory the thread factory.
155      * @param timeout       the timeout Duration for the process. It must be greater than 0 or {@code INFINITE_TIMEOUT_DURATION}.
156      */
157     private ExecuteWatchdog(final ThreadFactory threadFactory, final Duration timeout) {
158         this.killedProcess = false;
159         this.watch = false;
160         this.hasWatchdog = !INFINITE_TIMEOUT_DURATION.equals(timeout);
161         this.processStarted = false;
162         this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory();
163         if (this.hasWatchdog) {
164             this.watchdog = Watchdog.builder().setThreadFactory(this.threadFactory).setTimeout(timeout).get();
165             this.watchdog.addTimeoutObserver(this);
166         } else {
167             this.watchdog = null;
168         }
169     }
170 
171     /**
172      * This method will rethrow the exception that was possibly caught during the run of the process. It will only remains valid once the process has been
173      * terminated either by 'error', timeout or manual intervention. Information will be discarded once a new process is ran.
174      *
175      * @throws Exception a wrapped exception over the one that was silently swallowed and stored during the process run.
176      */
177     public synchronized void checkException() throws Exception {
178         if (caught != null) {
179             throw caught;
180         }
181     }
182 
183     /**
184      * reset the monitor flag and the process.
185      */
186     protected synchronized void cleanUp() {
187         watch = false;
188         process = null;
189     }
190 
191     /**
192      * Destroys the running process manually.
193      */
194     public synchronized void destroyProcess() {
195         ensureStarted();
196         timeoutOccured(null);
197         stop();
198     }
199 
200     /**
201      * Ensures that the process is started or not already terminated so we do not race with asynch executionor hang forever. The caller of this method must be
202      * holding the lock on this.
203      */
204     private void ensureStarted() {
205         while (!processStarted && caught == null) {
206             try {
207                 wait();
208             } catch (final InterruptedException e) {
209                 throw new IllegalStateException(e.getMessage(), e);
210             }
211         }
212     }
213 
214     /**
215      * Notification that starting the process failed.
216      *
217      * @param e the offending exception.
218      *
219      */
220     public synchronized void failedToStart(final Exception e) {
221         processStarted = true;
222         caught = e;
223         notifyAll();
224     }
225 
226     /**
227      * Indicates whether or not the watchdog is still monitoring the process.
228      *
229      * @return {@code true} if the process is still running, otherwise {@code false}.
230      */
231     public synchronized boolean isWatching() {
232         ensureStarted();
233         return watch;
234     }
235 
236     /**
237      * Indicates whether the last process run was killed.
238      *
239      * @return {@code true} if the process was killed {@code false}.
240      */
241     public synchronized boolean killedProcess() {
242         return killedProcess;
243     }
244 
245     void setProcessNotStarted() {
246         processStarted = false;
247     }
248 
249     /**
250      * Watches the given process and terminates it, if it runs for too long. All information from the previous run are reset.
251      *
252      * @param processToMonitor the process to monitor. It cannot be {@code null}.
253      * @throws IllegalStateException if a process is still being monitored.
254      */
255     public synchronized void start(final Process processToMonitor) {
256         Objects.requireNonNull(processToMonitor, "processToMonitor");
257         if (process != null) {
258             throw new IllegalStateException("Already running.");
259         }
260         caught = null;
261         killedProcess = false;
262         watch = true;
263         process = processToMonitor;
264         processStarted = true;
265         notifyAll();
266         if (hasWatchdog) {
267             watchdog.start();
268         }
269     }
270 
271     /**
272      * Stops the watcher. It will notify all threads possibly waiting on this object.
273      */
274     public synchronized void stop() {
275         if (hasWatchdog) {
276             watchdog.stop();
277         }
278         watch = false;
279         process = null;
280     }
281 
282     /**
283      * Called after watchdog has finished.
284      */
285     @Override
286     public synchronized void timeoutOccured(final Watchdog w) {
287         try {
288             try {
289                 // We must check if the process was not stopped
290                 // before being here
291                 if (process != null) {
292                     process.exitValue();
293                 }
294             } catch (final IllegalThreadStateException itse) {
295                 // the process is not terminated, if this is really
296                 // a timeout and not a manual stop then destroy it.
297                 if (watch) {
298                     killedProcess = true;
299                     process.destroy();
300                 }
301             }
302         } catch (final Exception e) {
303             caught = e;
304             DebugUtils.handleException("Getting the exit value of the process failed", e);
305         } finally {
306             cleanUp();
307         }
308     }
309 }