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