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