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    @Override
216    public void shutdown() {
217        shutdown(0, true);
218    }
219
220    /**
221     * {@inheritDoc}
222     */
223    @Override
224    public void restart() {
225        shutdown(100, true);
226    }
227
228    /**
229     * {@inheritDoc}
230     */
231    @Override
232    public void restartOS() {
233        shutdown(210, true);
234    }
235
236    /**
237     * {@inheritDoc}
238     */
239    @Override
240    public void shutdownOS() {
241        shutdown(200, true);
242    }
243
244    /**
245     * First asks the shutdown tasks if shutdown is allowed.
246     * Returns if the shutdown was aborted by the user, in which case the program
247     * should continue to operate.
248     * <p>
249     * After this check does not return under normal circumstances.
250     * Closes any displayable windows.
251     * Executes all registered {@link jmri.ShutDownTask}
252     * Runs the Early shutdown tasks, the main shutdown tasks,
253     * then terminates the program with provided status.
254     *
255     * @param status integer status on program exit
256     * @param exit   true if System.exit() should be called if all tasks are
257     *               executed correctly; false otherwise
258     */
259    public void shutdown(int status, boolean exit) {
260        Runnable shutdownTask = () -> { doShutdown(status, exit); };
261
262        if (!blockingShutdown) {
263            new Thread(shutdownTask).start();
264        } else {
265            shutdownTask.run();
266        }
267    }
268
269    /**
270     * First asks the shutdown tasks if shutdown is allowed.
271     * Returns if the shutdown was aborted by the user, in which case the program
272     * should continue to operate.
273     * <p>
274     * After this check does not return under normal circumstances.
275     * Closes any displayable windows.
276     * Executes all registered {@link jmri.ShutDownTask}
277     * Runs the Early shutdown tasks, the main shutdown tasks,
278     * then terminates the program with provided status.
279     * <p>
280     *
281     * @param status integer status on program exit
282     * @param exit   true if System.exit() should be called if all tasks are
283     *               executed correctly; false otherwise
284     */
285    @SuppressFBWarnings(value = "DM_EXIT", justification = "OK to directly exit standalone main")
286    private void doShutdown(int status, boolean exit) {
287        log.debug("shutdown called with {} {}", status, exit);
288        if (!shuttingDown) {
289            Date start = new Date();
290            log.debug("Shutting down with {} callable and {} runnable tasks",
291                callables.size(), runnables.size());
292            setShuttingDown(true);
293            // First check if shut down is allowed
294            for (Callable<Boolean> task : callables) {
295                try {
296                    if (Boolean.FALSE.equals(task.call())) {
297                        setShuttingDown(false);
298                        return;
299                    }
300                } catch (Exception ex) {
301                    log.error("Unable to stop", ex);
302                    setShuttingDown(false);
303                    return;
304                }
305            }
306            // close any open windows by triggering a closing event
307            // this gives open windows a final chance to perform any cleanup
308            if (!GraphicsEnvironment.isHeadless()) {
309                Arrays.asList(Frame.getFrames()).stream().forEach(frame -> {
310                    // do not run on thread, or in parallel, as System.exit()
311                    // will get called before windows can close
312                    if (frame.isDisplayable()) { // dispose() has not been called
313                        log.debug("Closing frame \"{}\", title: \"{}\"", frame.getName(), frame.getTitle());
314                        Date timer = new Date();
315                        frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));
316                        log.debug("Frame \"{}\" took {} milliseconds to close", frame.getName(), new Date().getTime() - timer.getTime());
317                    }
318                });
319            }
320            log.debug("windows completed closing {} milliseconds after starting shutdown", new Date().getTime() - start.getTime());
321
322            // wait for parallel tasks to complete
323            runShutDownTasks(new HashSet<>(earlyRunnables), "JMRI ShutDown - Early Tasks");
324
325            jmri.util.ThreadingUtil.runOnGUI(() -> {
326                jmri.configurexml.StoreAndCompare.requestStoreIfNeeded();
327            });
328
329            // wait for parallel tasks to complete
330            runShutDownTasks(runnables, "JMRI ShutDown - Main Tasks");
331
332            // success
333            log.debug("Shutdown took {} milliseconds.", new Date().getTime() - start.getTime());
334            log.info("Normal termination complete");
335            // and now terminate forcefully
336            if (exit) {
337                System.exit(status);
338            }
339        }
340    }
341
342    // blocks the main Thread until tasks complete or timed out
343    private void runShutDownTasks(Set<Runnable> toRun, String threadName ) {
344        Set<Runnable> sDrunnables = new HashSet<>(toRun); // copy list so cannot be modified
345        if ( sDrunnables.isEmpty() ) {
346            return;
347        }
348        // use a custom Executor which checks the Task output for Exceptions.
349        ExecutorService executor = new ShutDownThreadPoolExecutor(sDrunnables.size(), threadName);
350        List<Future<?>> complete = new ArrayList<>();
351        long timeoutEnd = new Date().getTime()+ tasksTimeOutMilliSec;
352
353
354        sDrunnables.forEach((runnable) -> {
355             complete.add(executor.submit(runnable));
356        });
357
358        executor.shutdown(); // no more tasks allowed from here, starts the threads.
359        while (!executor.isTerminated() && ( timeoutEnd > new Date().getTime() )) {
360            try {
361                // awaitTermination blocks the thread, checking occasionally
362                executor.awaitTermination(50, TimeUnit.MILLISECONDS);
363            } catch (InterruptedException e) {
364                // ignore
365            }
366        }
367
368        // so we're either all complete or timed out.
369        // check if a task timed out, if so log.
370        for ( Future<?> future : complete) {
371            if ( !future.isDone() ) {
372                log.error("Could not complete Shutdown Task in time: {} ", future );
373            }
374        }
375
376        executor.shutdownNow(); // do not leave Threads hanging before exit, force stop.
377
378    }
379
380    /**
381     * {@inheritDoc}
382     */
383    @Override
384    public boolean isShuttingDown() {
385        return shuttingDown;
386    }
387
388    /**
389     * This method is static so that if multiple DefaultShutDownManagers are
390     * registered, they are all aware of this state.
391     *
392     * @param state true if shutting down; false otherwise
393     */
394    protected void setShuttingDown(boolean state) {
395        boolean old = shuttingDown;
396        setStaticShuttingDown(state);
397        log.debug("Setting shuttingDown to {}", state);
398        firePropertyChange(PROP_SHUTTING_DOWN, old, state);
399    }
400
401    // package private so tests can reset
402    synchronized static void setStaticShuttingDown(boolean state){
403        shuttingDown = state;
404    }
405
406    private static class ShutDownThreadPoolExecutor extends ThreadPoolExecutor {
407
408        // use up to 8 threads for parallel tasks
409        // 10 seconds for tasks to enter a thread from queue
410        // set thread name with custom ThreadFactory
411        private ShutDownThreadPoolExecutor(int numberTasks, String threadName) {
412            super(8, 8, 10, TimeUnit.SECONDS,
413                new ArrayBlockingQueue<Runnable>(numberTasks), new ShutDownThreadFactory(threadName));
414        }
415
416        @Override
417        public void afterExecute(Runnable r, Throwable t) {
418            super.afterExecute(r, t);
419            // System.out.println("afterExecute "+ r);
420            if (t == null && r instanceof Future<?>) {
421                try {
422                    Future<?> future = (Future<?>) r;
423                    if (future.isDone()) {
424                        future.get();
425                    }
426                } catch (CancellationException ce) {
427                    t = ce;
428                } catch (ExecutionException ee) {
429                    t = ee.getCause();
430                } catch (InterruptedException ie) {
431                    Thread.currentThread().interrupt();
432                }
433            }
434            if (t != null) {
435                log.error("Issue Completing ShutdownTask : ", t);
436            }
437        }
438    }
439
440    private static class ShutDownThreadFactory implements ThreadFactory {
441
442        private final String threadName;
443
444        private ShutDownThreadFactory( String threadName ){
445            super();
446            this.threadName = threadName;
447        }
448
449        @Override
450        public Thread newThread(Runnable r) {
451            return new Thread(r, threadName);
452        }
453    }
454
455    private static class EarlyTask implements Runnable {
456
457        final ShutDownTask task; // access outside of this class
458
459        EarlyTask( ShutDownTask runnableTask) {
460            task = runnableTask;
461        }
462
463        @Override
464        public void run() {
465            task.runEarly();
466        }
467
468        @Override // improve error message on failure
469        public String toString(){
470            return task.toString();
471        }
472
473    }
474
475    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultShutDownManager.class);
476
477}