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}