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}