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 closeFrames(start); 295 296 // wait for parallel tasks to complete 297 runShutDownTasks(new HashSet<>(earlyRunnables), "JMRI ShutDown - Early Tasks"); 298 299 jmri.configurexml.StoreAndCompare.requestStoreIfNeeded(); 300 301 // wait for parallel tasks to complete 302 runShutDownTasks(runnables, "JMRI ShutDown - Main Tasks"); 303 304 // success 305 log.debug("Shutdown took {} milliseconds.", System.currentTimeMillis() - start); 306 log.info("Normal termination complete"); 307 // and now terminate forcefully 308 if (exit) { 309 System.exit(status); 310 } 311 shutDownComplete = true; 312 } 313 } 314 315 private void closeFrames( long startTime ) { 316 // close any open windows by triggering a closing event 317 // this gives open windows a final chance to perform any cleanup 318 if (!GraphicsEnvironment.isHeadless()) { 319 Arrays.asList(Frame.getFrames()).stream().forEach(frame -> { 320 // do not run on thread, or in parallel, as System.exit() 321 // will get called before windows can close 322 if (frame.isDisplayable()) { // dispose() has not been called 323 log.debug("Closing frame \"{}\", title: \"{}\"", frame.getName(), frame.getTitle()); 324 long timer = System.currentTimeMillis(); 325 frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING)); 326 log.debug("Frame \"{}\" took {} milliseconds to close", 327 frame.getName(), System.currentTimeMillis() - timer); 328 } 329 }); 330 } 331 log.debug("windows completed closing {} milliseconds after starting shutdown", 332 System.currentTimeMillis() - startTime ); 333 } 334 335 // blocks the main Thread until tasks complete or timed out 336 private void runShutDownTasks(Set<Runnable> toRun, String threadName ) { 337 Set<Runnable> sDrunnables = new HashSet<>(toRun); // copy list so cannot be modified 338 if ( sDrunnables.isEmpty() ) { 339 return; 340 } 341 // use a custom Executor which checks the Task output for Exceptions. 342 JmriThreadPoolExecutor executor = new JmriThreadPoolExecutor(sDrunnables.size(), threadName); 343 List<Future<?>> complete = new ArrayList<>(); 344 long timeoutEnd = System.currentTimeMillis() + tasksTimeOutMilliSec; 345 346 sDrunnables.forEach( runnable -> complete.add(executor.submit(runnable))); 347 348 executor.shutdown(); // no more tasks allowed from here, starts the threads. 349 350 // Handle individual task timeouts 351 for (Future<?> future : complete) { 352 long remainingTime = timeoutEnd - System.currentTimeMillis(); // Calculate remaining time 353 354 if (remainingTime <= 0) { 355 log.error("Timeout reached before all tasks were completed {} {}", threadName, future); 356 break; 357 } 358 359 try { 360 // Attempt to get the result of each task within the remaining time 361 future.get(remainingTime, TimeUnit.MILLISECONDS); 362 } catch (TimeoutException te) { 363 log.error("{} Task timed out: {}", threadName, future); 364 } catch (InterruptedException ie) { 365 Thread.currentThread().interrupt(); 366 // log.error("{} Task was interrupted: {}", threadName, future); 367 } catch (ExecutionException ee) { 368 // log.error("{} Task threw an exception: {}", threadName, future, ee.getCause()); 369 } 370 } 371 372 executor.shutdownNow(); // do not leave Threads hanging before exit, force stop. 373 374 } 375 376 /** 377 * {@inheritDoc} 378 */ 379 @Override 380 public boolean isShuttingDown() { 381 return shuttingDown; 382 } 383 384 /** 385 * Flag to indicate when all shutDown tasks completed. 386 * For test purposes, the app would normally exit before setting the flag. 387 * @return true when Shutdown tasks are complete and System.exit is not called. 388 */ 389 public boolean isShutDownComplete() { 390 return shutDownComplete; 391 } 392 393 /** 394 * This method is static so that if multiple DefaultShutDownManagers are 395 * registered, they are all aware of this state. 396 * 397 * @param state true if shutting down; false otherwise 398 */ 399 protected void setShuttingDown(boolean state) { 400 boolean old = shuttingDown; 401 setStaticShuttingDown(state); 402 log.debug("Setting shuttingDown to {}", state); 403 if ( !state ) { // reset complete if previously set 404 shutDownComplete = false; 405 } 406 firePropertyChange(PROP_SHUTTING_DOWN, old, state); 407 } 408 409 // package private so tests can reset 410 static synchronized void setStaticShuttingDown(boolean state){ 411 shuttingDown = state; 412 } 413 414 private static class EarlyTask implements Runnable { 415 416 final ShutDownTask task; // access outside of this class 417 418 EarlyTask( ShutDownTask runnableTask) { 419 task = runnableTask; 420 } 421 422 @Override 423 public void run() { 424 task.runEarly(); 425 } 426 427 @Override // improve error message on failure 428 public String toString(){ 429 return task.toString(); 430 } 431 432 } 433 434 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultShutDownManager.class); 435 436}