    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) &amp;&amp; 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}