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