    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}