1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * https://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20 package org.apache.commons.exec;
21
22 import java.time.Duration;
23 import java.util.ArrayList;
24 import java.util.List;
25 import java.util.concurrent.Executors;
26 import java.util.concurrent.ThreadFactory;
27 import java.util.function.Supplier;
28
29 /**
30 * Generalization of {@code ExecuteWatchdog}.
31 *
32 * @see org.apache.commons.exec.ExecuteWatchdog
33 */
34 public class Watchdog implements Runnable {
35
36 /**
37 * Builds ExecuteWatchdog instances.
38 *
39 * @since 1.4.0
40 */
41 public static final class Builder implements Supplier<Watchdog> {
42
43 /**
44 * Default timeout.
45 */
46 private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
47
48 /** Thread factory. */
49 private ThreadFactory threadFactory = Executors.defaultThreadFactory();
50
51 /**
52 * Timeout duration.
53 */
54 private Duration timeout = DEFAULT_TIMEOUT;
55
56 /**
57 * Constructs a new instance.
58 */
59 public Builder() {
60 // empty
61 }
62
63 /**
64 * Creates a new configured ExecuteWatchdog.
65 *
66 * @return a new configured ExecuteWatchdog.
67 */
68 @Override
69 public Watchdog get() {
70 return new Watchdog(this);
71 }
72
73 /**
74 * Sets the thread factory.
75 *
76 * @param threadFactory the thread factory, null resets to the default {@link Executors#defaultThreadFactory()}.
77 * @return {@code this} instance.
78 */
79 public Builder setThreadFactory(final ThreadFactory threadFactory) {
80 this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory();
81 return this;
82 }
83
84 /**
85 * Sets the timeout duration.
86 *
87 * @param timeout the timeout duration, null resets to the default 30 seconds timeout.
88 * @return {@code this} instance.
89 */
90 public Builder setTimeout(final Duration timeout) {
91 this.timeout = timeout != null ? timeout : DEFAULT_TIMEOUT;
92 return this;
93 }
94
95 }
96
97 /**
98 * Creates a new builder.
99 *
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 }