001package jmri.managers; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.awt.Frame; 006import java.awt.GraphicsEnvironment; 007import java.awt.event.WindowEvent; 008 009import java.util.*; 010import java.util.concurrent.*; 011 012import jmri.ShutDownManager; 013import jmri.ShutDownTask; 014import jmri.util.SystemType; 015import jmri.util.JmriThreadPoolExecutor; 016 017import jmri.beans.Bean; 018import jmri.util.ThreadingUtil; 019 020/** 021 * The default implementation of {@link ShutDownManager}. This implementation 022 * makes the following assumptions: 023 * <ul> 024 * <li>The {@link #shutdown()} and {@link #restart()} methods are called on the 025 * application's main thread.</li> 026 * <li>If the application has a graphical user interface, the application's main 027 * thread is the event dispatching thread.</li> 028 * <li>Application windows may contain code that <em>should</em> be run within a 029 * registered {@link ShutDownTask#run()} method, but are not. A side effect 030 * of this assumption is that <em>all</em> displayable application windows are 031 * closed by this implementation when shutdown() or restart() is called and a 032 * ShutDownTask has not aborted the shutdown or restart.</li> 033 * <li>It is expected that SIGINT and SIGTERM should trigger a clean application 034 * exit.</li> 035 * </ul> 036 * <p> 037 * If another implementation of ShutDownManager has not been registered with the 038 * {@link jmri.InstanceManager}, an instance of this implementation will be 039 * automatically registered as the ShutDownManager. 040 * <p> 041 * Developers other applications that cannot accept the above assumptions are 042 * recommended to create their own implementations of ShutDownManager that 043 * integrates with their application's lifecycle and register that 044 * implementation with the InstanceManager as soon as possible in their 045 * application. 046 * 047 * @author Bob Jacobsen Copyright (C) 2008 048 */ 049public class DefaultShutDownManager extends Bean implements ShutDownManager { 050 051 private static volatile boolean shuttingDown = false; 052 private volatile boolean shutDownComplete = false; // used by tests 053 054 private final Set<Callable<Boolean>> callables = new CopyOnWriteArraySet<>(); 055 private final Set<EarlyTask> earlyRunnables = new CopyOnWriteArraySet<>(); 056 private final Set<Runnable> runnables = new CopyOnWriteArraySet<>(); 057 058 protected final Thread shutdownHook; 059 060 // 30secs to complete EarlyTasks, 30 secs to complete Main tasks. 061 // package private for testing 062 int tasksTimeOutMilliSec = 30000; 063 064 private static final String NO_NULL_TASK = "Shutdown task cannot be null."; // NOI18N 065 private static final String PROP_SHUTTING_DOWN = "shuttingDown"; // NOI18N 066 067 private boolean blockingShutdown = false; // Used by tests 068 069 /** 070 * Create a new shutdown manager. 071 */ 072 public DefaultShutDownManager() { 073 super(false); 074 // This shutdown hook allows us to perform a clean shutdown when 075 // running in headless mode and SIGINT (Ctrl-C) or SIGTERM. It 076 // executes the shutdown tasks without calling System.exit() since 077 // calling System.exit() within a shutdown hook will cause the 078 // application to hang. 079 // This shutdown hook also allows OS X Application->Quit to trigger our 080 // shutdown tasks, since that simply calls System.exit() 081 this.shutdownHook = ThreadingUtil.newThread(() -> DefaultShutDownManager.this.shutdown(0, false)); 082 try { 083 Runtime.getRuntime().addShutdownHook(this.shutdownHook); 084 } catch (IllegalStateException ex) { 085 // thrown only if System.exit() has been called, so ignore 086 } 087 088 // register a Signal handlers that do shutdown 089 try { 090 if (SystemType.isMacOSX() || SystemType.isLinux()) { 091 sun.misc.Signal.handle(new sun.misc.Signal("INT"), sig -> shutdown()); 092 sun.misc.Signal.handle(new sun.misc.Signal("HUP"), sig -> restart()); 093 } 094 sun.misc.Signal.handle(new sun.misc.Signal("TERM"), sig -> shutdown()); 095 096 } catch (NullPointerException e) { 097 log.warn("Failed to add signal handler due to missing signal definition"); 098 } 099 } 100 101 /** 102 * Set if shutdown should block GUI/Layout thread. 103 * @param value true if blocking, false otherwise 104 */ 105 public void setBlockingShutdown(boolean value) { 106 blockingShutdown = value; 107 } 108 109 /** 110 * {@inheritDoc} 111 */ 112 @Override 113 public synchronized void register(ShutDownTask s) { 114 Objects.requireNonNull(s, NO_NULL_TASK); 115 this.earlyRunnables.add(new EarlyTask(s)); 116 this.runnables.add(s); 117 this.callables.add(s); 118 this.addPropertyChangeListener(PROP_SHUTTING_DOWN, s); 119 } 120 121 /** 122 * {@inheritDoc} 123 */ 124 @Override 125 public synchronized void register(Callable<Boolean> task) { 126 Objects.requireNonNull(task, NO_NULL_TASK); 127 this.callables.add(task); 128 } 129 130 /** 131 * {@inheritDoc} 132 */ 133 @Override 134 public synchronized void register(Runnable task) { 135 Objects.requireNonNull(task, NO_NULL_TASK); 136 this.runnables.add(task); 137 } 138 139 /** 140 * {@inheritDoc} 141 */ 142 @Override 143 public synchronized void deregister(ShutDownTask s) { 144 this.removePropertyChangeListener(PROP_SHUTTING_DOWN, s); 145 this.callables.remove(s); 146 this.runnables.remove(s); 147 for (EarlyTask r : earlyRunnables) { 148 if (r.task == s) { 149 earlyRunnables.remove(r); 150 } 151 } 152 } 153 154 /** 155 * {@inheritDoc} 156 */ 157 @Override 158 public synchronized void deregister(Callable<Boolean> task) { 159 this.callables.remove(task); 160 } 161 162 /** 163 * {@inheritDoc} 164 */ 165 @Override 166 public synchronized void deregister(Runnable task) { 167 this.runnables.remove(task); 168 } 169 170 /** 171 * {@inheritDoc} 172 */ 173 @Override 174 public List<Callable<Boolean>> getCallables() { 175 List<Callable<Boolean>> list = new ArrayList<>(); 176 list.addAll(callables); 177 return Collections.unmodifiableList(list); 178 } 179 180 /** 181 * {@inheritDoc} 182 */ 183 @Override 184 public List<Runnable> getRunnables() { 185 List<Runnable> list = new ArrayList<>(); 186 list.addAll(runnables); 187 return Collections.unmodifiableList(list); 188 } 189 190 /** 191 * {@inheritDoc} 192 */ 193 @Override 194 public void shutdown() { 195 shutdown(0, true); 196 } 197 198 /** 199 * {@inheritDoc} 200 */ 201 @Override 202 public void restart() { 203 shutdown(100, true); 204 } 205 206 /** 207 * {@inheritDoc} 208 */ 209 @Override 210 public void restartOS() { 211 shutdown(210, true); 212 } 213 214 /** 215 * {@inheritDoc} 216 */ 217 @Override 218 public void shutdownOS() { 219 shutdown(200, true); 220 } 221 222 /** 223 * First asks the shutdown tasks if shutdown is allowed. 224 * Returns if the shutdown was aborted by the user, in which case the program 225 * should continue to operate. 226 * <p> 227 * After this check does not return under normal circumstances. 228 * Closes any displayable windows. 229 * Executes all registered {@link jmri.ShutDownTask} 230 * Runs the Early shutdown tasks, the main shutdown tasks, 231 * then terminates the program with provided status. 232 * 233 * @param status integer status on program exit 234 * @param exit true if System.exit() should be called if all tasks are 235 * executed correctly; false otherwise 236 */ 237 public void shutdown(int status, boolean exit) { 238 Runnable shutdownTask = () -> doShutdown(status, exit); 239 240 if (!blockingShutdown) { 241 new Thread(shutdownTask).start(); 242 } else { 243 shutdownTask.run(); 244 } 245 } 246 247 /** 248 * First asks the shutdown tasks if shutdown is allowed. 249 * Returns if the shutdown was aborted by the user, in which case the program 250 * should continue to operate. 251 * <p> 252 * After this check does not return under normal circumstances. 253 * Closes any displayable windows. 254 * Executes all registered {@link jmri.ShutDownTask} 255 * Runs the Early shutdown tasks, the main shutdown tasks, 256 * then terminates the program with provided status. 257 * <p> 258 * 259 * @param status integer status on program exit 260 * @param exit true if System.exit() should be called if all tasks are 261 * executed correctly; false otherwise 262 */ 263 @SuppressFBWarnings(value = "DM_EXIT", justification = "OK to directly exit standalone main") 264 private void doShutdown(int status, boolean exit) { 265 log.debug("shutdown called with {} {}", status, exit); 266 if (!shuttingDown) { 267 long start = System.currentTimeMillis(); 268 log.debug("Shutting down with {} callable and {} runnable tasks", 269 callables.size(), runnables.size()); 270 setShuttingDown(true); 271 // First check if shut down is allowed 272 for (Callable<Boolean> task : callables) { 273 try { 274 if (Boolean.FALSE.equals(task.call())) { 275 setShuttingDown(false); 276 return; 277 } 278 } catch (Exception ex) { 279 log.error("Unable to stop", ex); 280 setShuttingDown(false); 281 return; 282 } 283 } 284 285 boolean abort = jmri.util.ThreadingUtil.runOnGUIwithReturn(() -> { 286 return jmri.configurexml.StoreAndCompare.checkPermissionToStoreIfNeeded(); 287 }); 288 if (abort) { 289 log.info("User aborted the shutdown request due to not having permission to store changes"); 290 setShuttingDown(false); 291 return; 292 } 293 294 // When a store is requested, the Cancel option will cancel the shutdown. 295 if (jmri.configurexml.StoreAndCompare.requestStoreIfNeeded()) { 296 log.debug("User cancelled the store request which also cancels the shutdown"); 297 setShuttingDown(false); 298 return; 299 } 300 301 closeFrames(start); 302 303 // wait for parallel tasks to complete 304 runShutDownTasks(new HashSet<>(earlyRunnables), "JMRI ShutDown - Early Tasks"); 305 306 // wait for parallel tasks to complete 307 runShutDownTasks(runnables, "JMRI ShutDown - Main Tasks"); 308 309 // success 310 log.debug("Shutdown took {} milliseconds.", System.currentTimeMillis() - start); 311 log.info("Normal termination complete"); 312 // and now terminate forcefully 313 if (exit) { 314 System.exit(status); 315 } 316 shutDownComplete = true; 317 } 318 } 319 320 private void closeFrames( long startTime ) { 321 // close any open windows by triggering a closing event 322 // this gives open windows a final chance to perform any cleanup 323 if (!GraphicsEnvironment.isHeadless()) { 324 Arrays.asList(Frame.getFrames()).stream().forEach(frame -> { 325 // do not run on thread, or in parallel, as System.exit() 326 // will get called before windows can close 327 if (frame.isDisplayable()) { // dispose() has not been called 328 log.debug("Closing frame \"{}\", title: \"{}\"", frame.getName(), frame.getTitle()); 329 long timer = System.currentTimeMillis(); 330 frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING)); 331 log.debug("Frame \"{}\" took {} milliseconds to close", 332 frame.getName(), System.currentTimeMillis() - timer); 333 } 334 }); 335 } 336 log.debug("windows completed closing {} milliseconds after starting shutdown", 337 System.currentTimeMillis() - startTime ); 338 } 339 340 // blocks the main Thread until tasks complete or timed out 341 private void runShutDownTasks(Set<Runnable> toRun, String threadName ) { 342 Set<Runnable> sDrunnables = new HashSet<>(toRun); // copy list so cannot be modified 343 if ( sDrunnables.isEmpty() ) { 344 return; 345 } 346 // use a custom Executor which checks the Task output for Exceptions. 347 JmriThreadPoolExecutor executor = new JmriThreadPoolExecutor(sDrunnables.size(), threadName); 348 List<Future<?>> complete = new ArrayList<>(); 349 long timeoutEnd = System.currentTimeMillis() + tasksTimeOutMilliSec; 350 351 sDrunnables.forEach( runnable -> complete.add(executor.submit(runnable))); 352 353 executor.shutdown(); // no more tasks allowed from here, starts the threads. 354 355 // Handle individual task timeouts 356 for (Future<?> future : complete) { 357 long remainingTime = timeoutEnd - System.currentTimeMillis(); // Calculate remaining time 358 359 if (remainingTime <= 0) { 360 log.error("Timeout reached before all tasks were completed {} {}", threadName, future); 361 break; 362 } 363 364 try { 365 // Attempt to get the result of each task within the remaining time 366 future.get(remainingTime, TimeUnit.MILLISECONDS); 367 } catch (TimeoutException te) { 368 log.error("{} Task timed out: {}", threadName, future); 369 } catch (InterruptedException ie) { 370 Thread.currentThread().interrupt(); 371 // log.error("{} Task was interrupted: {}", threadName, future); 372 } catch (ExecutionException ee) { 373 // log.error("{} Task threw an exception: {}", threadName, future, ee.getCause()); 374 } 375 } 376 377 executor.shutdownNow(); // do not leave Threads hanging before exit, force stop. 378 379 } 380 381 /** 382 * {@inheritDoc} 383 */ 384 @Override 385 public boolean isShuttingDown() { 386 return shuttingDown; 387 } 388 389 /** 390 * Flag to indicate when all shutDown tasks completed. 391 * For test purposes, the app would normally exit before setting the flag. 392 * @return true when Shutdown tasks are complete and System.exit is not called. 393 */ 394 public boolean isShutDownComplete() { 395 return shutDownComplete; 396 } 397 398 /** 399 * This method is static so that if multiple DefaultShutDownManagers are 400 * registered, they are all aware of this state. 401 * 402 * @param state true if shutting down; false otherwise 403 */ 404 protected void setShuttingDown(boolean state) { 405 boolean old = shuttingDown; 406 setStaticShuttingDown(state); 407 log.debug("Setting shuttingDown to {}", state); 408 if ( !state ) { // reset complete if previously set 409 shutDownComplete = false; 410 } 411 firePropertyChange(PROP_SHUTTING_DOWN, old, state); 412 } 413 414 // package private so tests can reset 415 static synchronized void setStaticShuttingDown(boolean state){ 416 shuttingDown = state; 417 } 418 419 private static class EarlyTask implements Runnable { 420 421 final ShutDownTask task; // access outside of this class 422 423 EarlyTask( ShutDownTask runnableTask) { 424 task = runnableTask; 425 } 426 427 @Override 428 public void run() { 429 task.runEarly(); 430 } 431 432 @Override // improve error message on failure 433 public String toString(){ 434 return task.toString(); 435 } 436 437 } 438 439 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultShutDownManager.class); 440 441}