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