ExecuteWatchdog.java

  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. package org.apache.commons.exec;

  18. import java.time.Duration;
  19. import java.util.Objects;
  20. import java.util.concurrent.Executors;
  21. import java.util.concurrent.ThreadFactory;
  22. import java.util.function.Supplier;

  23. import org.apache.commons.exec.util.DebugUtils;

  24. /**
  25.  * Destroys a process running for too long. For example:
  26.  *
  27.  * <pre>
  28.  * ExecuteWatchdog watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofSeconds(30)).get();
  29.  * Executor executor = DefaultExecutor.builder().setExecuteStreamHandler(new PumpStreamHandler()).get();
  30.  * executor.setWatchdog(watchdog);
  31.  * int exitValue = executor.execute(myCommandLine);
  32.  * if (executor.isFailure(exitValue) &amp;&amp; watchdog.killedProcess()) {
  33.  *     // it was killed on purpose by the watchdog
  34.  * }
  35.  * </pre>
  36.  * <p>
  37.  * 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
  38.  * pass {@link #INFINITE_TIMEOUT_DURATION}) and to kill the process explicitly using {@link #destroyProcess()}.
  39.  * </p>
  40.  * <p>
  41.  * Please note that ExecuteWatchdog is processed asynchronously, e.g. it might be still attached to a process even after the
  42.  * {@link DefaultExecutor#execute(CommandLine)} or a variation has returned.
  43.  * </p>
  44.  *
  45.  * @see Executor
  46.  * @see Watchdog
  47.  */
  48. public class ExecuteWatchdog implements TimeoutObserver {

  49.     /**
  50.      * Builds ExecuteWatchdog instances.
  51.      *
  52.      * @since 1.4.0
  53.      */
  54.     public static final class Builder implements Supplier<ExecuteWatchdog> {

  55.         private ThreadFactory threadFactory;
  56.         private Duration timeout;

  57.         /**
  58.          * Constructs a new instance.
  59.          */
  60.         public Builder() {
  61.             // empty
  62.         }

  63.         /**
  64.          * Creates a new configured ExecuteWatchdog.
  65.          *
  66.          * @return a new configured ExecuteWatchdog.
  67.          */
  68.         @Override
  69.         public ExecuteWatchdog get() {
  70.             return new ExecuteWatchdog(threadFactory, timeout);
  71.         }

  72.         /**
  73.          * Sets the thread factory.
  74.          *
  75.          * @param threadFactory the thread factory.
  76.          * @return {@code this} instance.
  77.          */
  78.         public Builder setThreadFactory(final ThreadFactory threadFactory) {
  79.             this.threadFactory = threadFactory;
  80.             return this;
  81.         }

  82.         /**
  83.          * Sets the timeout duration.
  84.          *
  85.          * @param timeout the timeout duration.
  86.          * @return {@code this} instance.
  87.          */
  88.         public Builder setTimeout(final Duration timeout) {
  89.             this.timeout = timeout;
  90.             return this;
  91.         }

  92.     }

  93.     /** The marker for an infinite timeout. */
  94.     public static final long INFINITE_TIMEOUT = -1;

  95.     /** The marker for an infinite timeout. */
  96.     public static final Duration INFINITE_TIMEOUT_DURATION = Duration.ofMillis(INFINITE_TIMEOUT);

  97.     /**
  98.      * Creates a new builder.
  99.      *
  100.      * @return a new builder.
  101.      * @since 1.4.0
  102.      */
  103.     public static Builder builder() {
  104.         return new Builder();
  105.     }

  106.     /** The process to execute and watch for duration. */
  107.     private Process process;

  108.     /** Is a user-supplied timeout in use. */
  109.     private final boolean hasWatchdog;

  110.     /** Say whether or not the watchdog is currently monitoring a process. */
  111.     private boolean watch;

  112.     /** Exception that might be thrown during the process execution. */
  113.     private Exception caught;

  114.     /** Say whether or not the process was killed due to running overtime. */
  115.     private boolean killedProcess;

  116.     /** Will tell us whether timeout has occurred. */
  117.     private final Watchdog watchdog;

  118.     /** Indicates that the process is verified as started */
  119.     private volatile boolean processStarted;

  120.     /**
  121.      * The thread factory.
  122.      */
  123.     private final ThreadFactory threadFactory;

  124.     /**
  125.      * Creates a new watchdog with a given timeout.
  126.      *
  127.      * @param timeoutMillis the timeout for the process in milliseconds. It must be greater than 0 or {@code INFINITE_TIMEOUT}.
  128.      * @deprecated Use {@link Builder#get()}.
  129.      */
  130.     @Deprecated
  131.     public ExecuteWatchdog(final long timeoutMillis) {
  132.         this(Executors.defaultThreadFactory(), Duration.ofMillis(timeoutMillis));
  133.     }

  134.     /**
  135.      * Creates a new watchdog with a given timeout.
  136.      *
  137.      * @param threadFactory the thread factory.
  138.      * @param timeout       the timeout Duration for the process. It must be greater than 0 or {@code INFINITE_TIMEOUT_DURATION}.
  139.      */
  140.     private ExecuteWatchdog(final ThreadFactory threadFactory, final Duration timeout) {
  141.         this.killedProcess = false;
  142.         this.watch = false;
  143.         this.hasWatchdog = !INFINITE_TIMEOUT_DURATION.equals(timeout);
  144.         this.processStarted = false;
  145.         this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory();
  146.         if (this.hasWatchdog) {
  147.             this.watchdog = Watchdog.builder().setThreadFactory(this.threadFactory).setTimeout(timeout).get();
  148.             this.watchdog.addTimeoutObserver(this);
  149.         } else {
  150.             this.watchdog = null;
  151.         }
  152.     }

  153.     /**
  154.      * 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
  155.      * terminated either by 'error', timeout or manual intervention. Information will be discarded once a new process is ran.
  156.      *
  157.      * @throws Exception a wrapped exception over the one that was silently swallowed and stored during the process run.
  158.      */
  159.     public synchronized void checkException() throws Exception {
  160.         if (caught != null) {
  161.             throw caught;
  162.         }
  163.     }

  164.     /**
  165.      * reset the monitor flag and the process.
  166.      */
  167.     protected synchronized void cleanUp() {
  168.         watch = false;
  169.         process = null;
  170.     }

  171.     /**
  172.      * Destroys the running process manually.
  173.      */
  174.     public synchronized void destroyProcess() {
  175.         ensureStarted();
  176.         timeoutOccured(null);
  177.         stop();
  178.     }

  179.     /**
  180.      * 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
  181.      * holding the lock on this.
  182.      */
  183.     private void ensureStarted() {
  184.         while (!processStarted && caught == null) {
  185.             try {
  186.                 wait();
  187.             } catch (final InterruptedException e) {
  188.                 throw new IllegalStateException(e.getMessage(), e);
  189.             }
  190.         }
  191.     }

  192.     /**
  193.      * Notification that starting the process failed.
  194.      *
  195.      * @param e the offending exception.
  196.      */
  197.     public synchronized void failedToStart(final Exception e) {
  198.         processStarted = true;
  199.         caught = e;
  200.         notifyAll();
  201.     }

  202.     /**
  203.      * Indicates whether or not the watchdog is still monitoring the process.
  204.      *
  205.      * @return {@code true} if the process is still running, otherwise {@code false}.
  206.      */
  207.     public synchronized boolean isWatching() {
  208.         ensureStarted();
  209.         return watch;
  210.     }

  211.     /**
  212.      * Indicates whether the last process run was killed.
  213.      *
  214.      * @return {@code true} if the process was killed {@code false}.
  215.      */
  216.     public synchronized boolean killedProcess() {
  217.         return killedProcess;
  218.     }

  219.     void setProcessNotStarted() {
  220.         processStarted = false;
  221.     }

  222.     /**
  223.      * Watches the given process and terminates it, if it runs for too long. All information from the previous run are reset.
  224.      *
  225.      * @param processToMonitor the process to monitor. It cannot be {@code null}.
  226.      * @throws IllegalStateException if a process is still being monitored.
  227.      */
  228.     public synchronized void start(final Process processToMonitor) {
  229.         Objects.requireNonNull(processToMonitor, "processToMonitor");
  230.         if (process != null) {
  231.             throw new IllegalStateException("Already running.");
  232.         }
  233.         caught = null;
  234.         killedProcess = false;
  235.         watch = true;
  236.         process = processToMonitor;
  237.         processStarted = true;
  238.         notifyAll();
  239.         if (hasWatchdog) {
  240.             watchdog.start();
  241.         }
  242.     }

  243.     /**
  244.      * Stops the watcher. It will notify all threads possibly waiting on this object.
  245.      */
  246.     public synchronized void stop() {
  247.         if (hasWatchdog) {
  248.             watchdog.stop();
  249.         }
  250.         watch = false;
  251.         process = null;
  252.     }

  253.     /**
  254.      * Called after watchdog has finished.
  255.      */
  256.     @Override
  257.     public synchronized void timeoutOccured(final Watchdog w) {
  258.         try {
  259.             try {
  260.                 // We must check if the process was not stopped
  261.                 // before being here
  262.                 if (process != null) {
  263.                     process.exitValue();
  264.                 }
  265.             } catch (final IllegalThreadStateException itse) {
  266.                 // the process is not terminated, if this is really
  267.                 // a timeout and not a manual stop then destroy it.
  268.                 if (watch) {
  269.                     killedProcess = true;
  270.                     process.destroy();
  271.                 }
  272.             }
  273.         } catch (final Exception e) {
  274.             caught = e;
  275.             DebugUtils.handleException("Getting the exit value of the process failed", e);
  276.         } finally {
  277.             cleanUp();
  278.         }
  279.     }
  280. }