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}