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}