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}