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