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