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 }