001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020package org.apache.commons.exec; 021 022import java.time.Duration; 023import java.util.Objects; 024import java.util.concurrent.Executors; 025import java.util.concurrent.ThreadFactory; 026import java.util.function.Supplier; 027 028import org.apache.commons.exec.util.DebugUtils; 029 030/** 031 * Destroys a process running for too long. For example: 032 * 033 * <pre> 034 * ExecuteWatchdog watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofSeconds(30)).get(); 035 * Executor executor = DefaultExecutor.builder().setExecuteStreamHandler(new PumpStreamHandler()).get(); 036 * executor.setWatchdog(watchdog); 037 * int exitValue = executor.execute(myCommandLine); 038 * if (executor.isFailure(exitValue) && watchdog.killedProcess()) { 039 * // it was killed on purpose by the watchdog 040 * } 041 * </pre> 042 * <p> 043 * 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 044 * pass {@link #INFINITE_TIMEOUT_DURATION}) and to kill the process explicitly using {@link #destroyProcess()}. 045 * </p> 046 * <p> 047 * Please note that ExecuteWatchdog is processed asynchronously, e.g. it might be still attached to a process even after the 048 * {@link DefaultExecutor#execute(CommandLine)} or a variation has returned. 049 * </p> 050 * 051 * @see Executor 052 * @see Watchdog 053 */ 054public class ExecuteWatchdog implements TimeoutObserver { 055 056 /** 057 * Builds ExecuteWatchdog instances. 058 * 059 * @since 1.4.0 060 */ 061 public static final class Builder implements Supplier<ExecuteWatchdog> { 062 063 /** Thread factory. */ 064 private ThreadFactory threadFactory = Executors.defaultThreadFactory(); 065 066 /** Timeout duration. */ 067 private Duration timeout = INFINITE_TIMEOUT_DURATION; 068 069 /** 070 * Constructs a new instance. 071 */ 072 public Builder() { 073 // empty 074 } 075 076 /** 077 * Creates a new configured ExecuteWatchdog. 078 * 079 * @return a new configured ExecuteWatchdog. 080 */ 081 @Override 082 public ExecuteWatchdog get() { 083 return new ExecuteWatchdog(this); 084 } 085 086 /** 087 * Sets the thread factory. 088 * 089 * @param threadFactory the thread factory, null resets to the default {@link Executors#defaultThreadFactory()}. 090 * @return {@code this} instance. 091 */ 092 public Builder setThreadFactory(final ThreadFactory threadFactory) { 093 this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory(); 094 return this; 095 } 096 097 /** 098 * Sets the timeout duration. 099 * 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}