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