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}