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.ArrayList;
024import java.util.List;
025import java.util.concurrent.Executors;
026import java.util.concurrent.ThreadFactory;
027import java.util.function.Supplier;
028
029/**
030 * Generalization of {@code ExecuteWatchdog}.
031 *
032 * @see org.apache.commons.exec.ExecuteWatchdog
033 */
034public class Watchdog implements Runnable {
035
036    /**
037     * Builds ExecuteWatchdog instances.
038     *
039     * @since 1.4.0
040     */
041    public static final class Builder implements Supplier<Watchdog> {
042
043        /**
044         * Default timeout.
045         */
046        private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
047
048        /** Thread factory. */
049        private ThreadFactory threadFactory = Executors.defaultThreadFactory();
050
051        /**
052         * Timeout duration.
053         */
054        private Duration timeout = DEFAULT_TIMEOUT;
055
056        /**
057         * Constructs a new instance.
058         */
059        public Builder() {
060            // empty
061        }
062
063        /**
064         * Creates a new configured ExecuteWatchdog.
065         *
066         * @return a new configured ExecuteWatchdog.
067         */
068        @Override
069        public Watchdog get() {
070            return new Watchdog(this);
071        }
072
073        /**
074         * Sets the thread factory.
075         *
076         * @param threadFactory the thread factory, null resets to the default {@link Executors#defaultThreadFactory()}.
077         * @return {@code this} instance.
078         */
079        public Builder setThreadFactory(final ThreadFactory threadFactory) {
080            this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory();
081            return this;
082        }
083
084        /**
085         * Sets the timeout duration.
086         *
087         * @param timeout the timeout duration, null resets to the default 30 seconds timeout.
088         * @return {@code this} instance.
089         */
090        public Builder setTimeout(final Duration timeout) {
091            this.timeout = timeout != null ? timeout : DEFAULT_TIMEOUT;
092            return this;
093        }
094
095    }
096
097    /**
098     * Creates a new builder.
099     *
100     * @return a new builder.
101     * @since 1.4.0
102     */
103    public static Builder builder() {
104        return new Builder();
105    }
106
107    /**
108     * Observers.
109     */
110    private final List<TimeoutObserver> observers = new ArrayList<>(1);
111
112    /**
113     * Timeout duration.
114     */
115    private final Duration timeout;
116
117    /**
118     * Whether this is stopped.
119     */
120    private boolean stopped;
121
122    /**
123     * The thread factory.
124     */
125    private final ThreadFactory threadFactory;
126
127    /**
128     * Constructs a new instance.
129     *
130     * @param threadFactory the thread factory.
131     * @param timeout       the timeout duration.
132     */
133    private Watchdog(final Builder builder) {
134        if (builder.timeout.isNegative() || Duration.ZERO.equals(builder.timeout)) {
135            throw new IllegalArgumentException("Timeout must be positive.");
136        }
137        this.timeout = builder.timeout;
138        this.threadFactory = builder.threadFactory;
139    }
140
141    /**
142     * Constructs a new instance.
143     *
144     * @param timeoutMillis the timeout duration.
145     * @deprecated Use {@link Builder#get()}.
146     */
147    @Deprecated
148    public Watchdog(final long timeoutMillis) {
149        this(builder().setTimeout(Duration.ofMillis(timeoutMillis)));
150    }
151
152    /**
153     * Adds a TimeoutObserver.
154     *
155     * @param to a TimeoutObserver to add.
156     */
157    public void addTimeoutObserver(final TimeoutObserver to) {
158        observers.add(to);
159    }
160
161    /**
162     * Fires a timeout occurred event for each observer.
163     */
164    protected final void fireTimeoutOccured() {
165        observers.forEach(o -> o.timeoutOccured(this));
166    }
167
168    /**
169     * Gets the thread factory.
170     *
171     * @return the thread factory.
172     */
173    ThreadFactory getThreadFactory() {
174        return threadFactory;
175    }
176
177    /**
178     * Gets the timeout.
179     *
180     * @return the timeout.
181     * @since 1.6.0
182     */
183    public Duration getTimeout() {
184        return timeout;
185    }
186
187    /**
188     * Removes a TimeoutObserver.
189     *
190     * @param to a TimeoutObserver to remove.
191     */
192    public void removeTimeoutObserver(final TimeoutObserver to) {
193        observers.remove(to);
194    }
195
196    @Override
197    public void run() {
198        final long startTimeMillis = System.currentTimeMillis();
199        boolean isWaiting;
200        synchronized (this) {
201            final long timeoutMillis = timeout.toMillis();
202            long timeLeftMillis = timeoutMillis - (System.currentTimeMillis() - startTimeMillis);
203            isWaiting = timeLeftMillis > 0;
204            while (!stopped && isWaiting) {
205                try {
206                    wait(timeLeftMillis);
207                } catch (final InterruptedException ignore) {
208                    // ignore
209                }
210                timeLeftMillis = timeoutMillis - (System.currentTimeMillis() - startTimeMillis);
211                isWaiting = timeLeftMillis > 0;
212            }
213        }
214        // notify the listeners outside of the synchronized block (see EXEC-60)
215        if (!isWaiting) {
216            fireTimeoutOccured();
217        }
218    }
219
220    /**
221     * Starts a new thread.
222     */
223    public synchronized void start() {
224        stopped = false;
225        ThreadUtil.newThread(threadFactory, this, "CommonsExecWatchdog-", true).start();
226    }
227
228    /**
229     * Requests a thread stop.
230     */
231    public synchronized void stop() {
232        stopped = true;
233        notifyAll();
234    }
235
236}