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) && 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 }