    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) &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         * 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}