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 * http://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 * Creates a new configured ExecuteWatchdog. 066 * 067 * @return a new configured ExecuteWatchdog. 068 */ 069 @Override 070 public ExecuteWatchdog get() { 071 return new ExecuteWatchdog(threadFactory, timeout); 072 } 073 074 /** 075 * Sets the thread factory. 076 * 077 * @param threadFactory the thread factory. 078 * @return this. 079 */ 080 public Builder setThreadFactory(final ThreadFactory threadFactory) { 081 this.threadFactory = threadFactory; 082 return this; 083 } 084 085 /** 086 * Sets the timeout duration. 087 * 088 * @param timeout the timeout duration. 089 * @return this. 090 */ 091 public Builder setTimeout(final Duration timeout) { 092 this.timeout = timeout; 093 return this; 094 } 095 096 } 097 098 /** The marker for an infinite timeout. */ 099 public static final long INFINITE_TIMEOUT = -1; 100 101 /** The marker for an infinite timeout. */ 102 public static final Duration INFINITE_TIMEOUT_DURATION = Duration.ofMillis(INFINITE_TIMEOUT); 103 104 /** 105 * Creates a new builder. 106 * 107 * @return a new builder. 108 * @since 1.4.0 109 */ 110 public static Builder builder() { 111 return new Builder(); 112 } 113 114 /** The process to execute and watch for duration. */ 115 private Process process; 116 117 /** Is a user-supplied timeout in use. */ 118 private final boolean hasWatchdog; 119 120 /** Say whether or not the watchdog is currently monitoring a process. */ 121 private boolean watch; 122 123 /** Exception that might be thrown during the process execution. */ 124 private Exception caught; 125 126 /** Say whether or not the process was killed due to running overtime. */ 127 private boolean killedProcess; 128 129 /** Will tell us whether timeout has occurred. */ 130 private final Watchdog watchdog; 131 132 /** Indicates that the process is verified as started */ 133 private volatile boolean processStarted; 134 135 /** 136 * The thread factory. 137 */ 138 private final ThreadFactory threadFactory; 139 140 /** 141 * Creates a new watchdog with a given timeout. 142 * 143 * @param timeoutMillis the timeout for the process in milliseconds. It must be greater than 0 or {@code INFINITE_TIMEOUT}. 144 * @deprecated Use {@link Builder#get()}. 145 */ 146 @Deprecated 147 public ExecuteWatchdog(final long timeoutMillis) { 148 this(Executors.defaultThreadFactory(), Duration.ofMillis(timeoutMillis)); 149 } 150 151 /** 152 * Creates a new watchdog with a given timeout. 153 * 154 * @param threadFactory the thread factory. 155 * @param timeout the timeout Duration for the process. It must be greater than 0 or {@code INFINITE_TIMEOUT_DURATION}. 156 */ 157 private ExecuteWatchdog(final ThreadFactory threadFactory, final Duration timeout) { 158 this.killedProcess = false; 159 this.watch = false; 160 this.hasWatchdog = !INFINITE_TIMEOUT_DURATION.equals(timeout); 161 this.processStarted = false; 162 this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory(); 163 if (this.hasWatchdog) { 164 this.watchdog = Watchdog.builder().setThreadFactory(this.threadFactory).setTimeout(timeout).get(); 165 this.watchdog.addTimeoutObserver(this); 166 } else { 167 this.watchdog = null; 168 } 169 } 170 171 /** 172 * 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 173 * terminated either by 'error', timeout or manual intervention. Information will be discarded once a new process is ran. 174 * 175 * @throws Exception a wrapped exception over the one that was silently swallowed and stored during the process run. 176 */ 177 public synchronized void checkException() throws Exception { 178 if (caught != null) { 179 throw caught; 180 } 181 } 182 183 /** 184 * reset the monitor flag and the process. 185 */ 186 protected synchronized void cleanUp() { 187 watch = false; 188 process = null; 189 } 190 191 /** 192 * Destroys the running process manually. 193 */ 194 public synchronized void destroyProcess() { 195 ensureStarted(); 196 timeoutOccured(null); 197 stop(); 198 } 199 200 /** 201 * 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 202 * holding the lock on this. 203 */ 204 private void ensureStarted() { 205 while (!processStarted && caught == null) { 206 try { 207 wait(); 208 } catch (final InterruptedException e) { 209 throw new IllegalStateException(e.getMessage(), e); 210 } 211 } 212 } 213 214 /** 215 * Notification that starting the process failed. 216 * 217 * @param e the offending exception. 218 * 219 */ 220 public synchronized void failedToStart(final Exception e) { 221 processStarted = true; 222 caught = e; 223 notifyAll(); 224 } 225 226 /** 227 * Indicates whether or not the watchdog is still monitoring the process. 228 * 229 * @return {@code true} if the process is still running, otherwise {@code false}. 230 */ 231 public synchronized boolean isWatching() { 232 ensureStarted(); 233 return watch; 234 } 235 236 /** 237 * Indicates whether the last process run was killed. 238 * 239 * @return {@code true} if the process was killed {@code false}. 240 */ 241 public synchronized boolean killedProcess() { 242 return killedProcess; 243 } 244 245 void setProcessNotStarted() { 246 processStarted = false; 247 } 248 249 /** 250 * Watches the given process and terminates it, if it runs for too long. All information from the previous run are reset. 251 * 252 * @param processToMonitor the process to monitor. It cannot be {@code null}. 253 * @throws IllegalStateException if a process is still being monitored. 254 */ 255 public synchronized void start(final Process processToMonitor) { 256 Objects.requireNonNull(processToMonitor, "processToMonitor"); 257 if (process != null) { 258 throw new IllegalStateException("Already running."); 259 } 260 caught = null; 261 killedProcess = false; 262 watch = true; 263 process = processToMonitor; 264 processStarted = true; 265 notifyAll(); 266 if (hasWatchdog) { 267 watchdog.start(); 268 } 269 } 270 271 /** 272 * Stops the watcher. It will notify all threads possibly waiting on this object. 273 */ 274 public synchronized void stop() { 275 if (hasWatchdog) { 276 watchdog.stop(); 277 } 278 watch = false; 279 process = null; 280 } 281 282 /** 283 * Called after watchdog has finished. 284 */ 285 @Override 286 public synchronized void timeoutOccured(final Watchdog w) { 287 try { 288 try { 289 // We must check if the process was not stopped 290 // before being here 291 if (process != null) { 292 process.exitValue(); 293 } 294 } catch (final IllegalThreadStateException itse) { 295 // the process is not terminated, if this is really 296 // a timeout and not a manual stop then destroy it. 297 if (watch) { 298 killedProcess = true; 299 process.destroy(); 300 } 301 } 302 } catch (final Exception e) { 303 caught = e; 304 DebugUtils.handleException("Getting the exit value of the process failed", e); 305 } finally { 306 cleanUp(); 307 } 308 } 309}