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}