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