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;
014
015import jmri.beans.Bean;
016import jmri.util.ThreadingUtil;
017
018/**
019 * The default implementation of {@link ShutDownManager}. This implementation
020 * makes the following assumptions:
021 * <ul>
022 * <li>The {@link #shutdown()} and {@link #restart()} methods are called on the
023 * application's main thread.</li>
024 * <li>If the application has a graphical user interface, the application's main
025 * thread is the event dispatching thread.</li>
026 * <li>Application windows may contain code that <em>should</em> be run within a
027 * registered {@link ShutDownTask#run()} method, but are not. A side effect
028 * of this assumption is that <em>all</em> displayable application windows are
029 * closed by this implementation when shutdown() or restart() is called and a
030 * ShutDownTask has not aborted the shutdown or restart.</li>
031 * <li>It is expected that SIGINT and SIGTERM should trigger a clean application
032 * exit.</li>
033 * </ul>
034 * <p>
035 * If another implementation of ShutDownManager has not been registered with the
036 * {@link jmri.InstanceManager}, an instance of this implementation will be
037 * automatically registered as the ShutDownManager.
038 * <p>
039 * Developers other applications that cannot accept the above assumptions are
040 * recommended to create their own implementations of ShutDownManager that
041 * integrates with their application's lifecycle and register that
042 * implementation with the InstanceManager as soon as possible in their
043 * application.
044 *
045 * @author Bob Jacobsen Copyright (C) 2008
046 */
047public class DefaultShutDownManager extends Bean implements ShutDownManager {
048
049    private static volatile boolean shuttingDown = false;
050
051    private final Set<Callable<Boolean>> callables = new CopyOnWriteArraySet<>();
052    private final Set<EarlyTask> earlyRunnables = new CopyOnWriteArraySet<>();
053    private final Set<Runnable> runnables = new CopyOnWriteArraySet<>();
054
055    protected final Thread shutdownHook;
056
057    // 30secs to complete EarlyTasks, 30 secs to complete Main tasks.
058    // package private for testing
059    int tasksTimeOutMilliSec = 30000; 
060
061    private static final String NO_NULL_TASK = "Shutdown task cannot be null."; // NOI18N
062    private static final String PROP_SHUTTING_DOWN = "shuttingDown"; // NOI18N
063
064    /**
065     * Create a new shutdown manager.
066     */
067    public DefaultShutDownManager() {
068        super(false);
069        // This shutdown hook allows us to perform a clean shutdown when
070        // running in headless mode and SIGINT (Ctrl-C) or SIGTERM. It
071        // executes the shutdown tasks without calling System.exit() since
072        // calling System.exit() within a shutdown hook will cause the
073        // application to hang.
074        // This shutdown hook also allows OS X Application->Quit to trigger our
075        // shutdown tasks, since that simply calls System.exit()
076        this.shutdownHook = ThreadingUtil.newThread(() -> DefaultShutDownManager.this.shutdown(0, false));
077        try {
078            Runtime.getRuntime().addShutdownHook(this.shutdownHook);
079        } catch (IllegalStateException ex) {
080            // thrown only if System.exit() has been called, so ignore
081        }
082    }
083
084    /**
085     * {@inheritDoc}
086     */
087    @Override
088    public synchronized void register(ShutDownTask s) {
089        Objects.requireNonNull(s, NO_NULL_TASK);
090        this.earlyRunnables.add(new EarlyTask(s));
091        this.runnables.add(s);
092        this.callables.add(s);
093        this.addPropertyChangeListener(PROP_SHUTTING_DOWN, s);
094    }
095
096    /**
097     * {@inheritDoc}
098     */
099    @Override
100    public synchronized void register(Callable<Boolean> task) {
101        Objects.requireNonNull(task, NO_NULL_TASK);
102        this.callables.add(task);
103    }
104
105    /**
106     * {@inheritDoc}
107     */
108    @Override
109    public synchronized void register(Runnable task) {
110        Objects.requireNonNull(task, NO_NULL_TASK);
111        this.runnables.add(task);
112    }
113
114    /**
115     * {@inheritDoc}
116     */
117    @Override
118    public synchronized void deregister(ShutDownTask s) {
119        this.removePropertyChangeListener(PROP_SHUTTING_DOWN, s);
120        this.callables.remove(s);
121        this.runnables.remove(s);
122        for (EarlyTask r : earlyRunnables) {
123            if (r.task == s) earlyRunnables.remove(r);
124        }
125    }
126
127    /**
128     * {@inheritDoc}
129     */
130    @Override
131    public synchronized void deregister(Callable<Boolean> task) {
132        this.callables.remove(task);
133    }
134
135    /**
136     * {@inheritDoc}
137     */
138    @Override
139    public synchronized void deregister(Runnable task) {
140        this.runnables.remove(task);
141    }
142
143    /**
144     * {@inheritDoc}
145     */
146    @Override
147    public List<Callable<Boolean>> getCallables() {
148        List<Callable<Boolean>> list = new ArrayList<>();
149        list.addAll(callables);
150        return Collections.unmodifiableList(list);
151    }
152
153    /**
154     * {@inheritDoc}
155     */
156    @Override
157    public List<Runnable> getRunnables() {
158        List<Runnable> list = new ArrayList<>();
159        list.addAll(runnables);
160        return Collections.unmodifiableList(list);
161    }
162
163    /**
164     * {@inheritDoc}
165     */
166    @SuppressFBWarnings(value = "DM_EXIT", justification = "OK to directly exit standalone main")
167    @Override
168    public boolean shutdown() {
169        return shutdown(0, true);
170    }
171
172    /**
173     * {@inheritDoc}
174     */
175    @SuppressFBWarnings(value = "DM_EXIT", justification = "OK to directly exit standalone main")
176    @Override
177    public boolean restart() {
178        return shutdown(100, true);
179    }
180
181    /**
182     * {@inheritDoc}
183     */
184    @SuppressFBWarnings(value = "DM_EXIT", justification = "OK to directly exit standalone main")
185    @Override
186    public boolean restartOS() {
187        return shutdown(210, true);
188    }
189
190    /**
191     * {@inheritDoc}
192     */
193    @SuppressFBWarnings(value = "DM_EXIT", justification = "OK to directly exit standalone main")
194    @Override
195    public boolean shutdownOS() {
196        return shutdown(200, true);
197    }
198
199    /**
200     * First asks the shutdown tasks if shutdown is allowed.
201     * If not, return false.
202     * Return false if the shutdown was aborted by the user, in which case the program
203     * should continue to operate.
204     * <p>
205     * After this check does not return under normal circumstances.
206     * Closes any displayable windows.
207     * Executes all registered {@link jmri.ShutDownTask}
208     * Runs the Early shutdown tasks, the main shutdown tasks,
209     * then terminates the program with provided status.
210     * <p>
211     *
212     * @param status integer status on program exit
213     * @param exit   true if System.exit() should be called if all tasks are
214     *               executed correctly; false otherwise
215     * @return false if shutdown or restart failed
216     */
217    @SuppressFBWarnings(value = "DM_EXIT", justification = "OK to directly exit standalone main")
218    protected boolean shutdown(int status, boolean exit) {
219        if (!shuttingDown) {
220            jmri.configurexml.StoreAndCompare.requestStoreIfNeeded();
221            Date start = new Date();
222            log.debug("Shutting down with {} callable and {} runnable tasks",
223                callables.size(), runnables.size());
224            setShuttingDown(true);
225            // First check if shut down is allowed
226            for (Callable<Boolean> task : callables) {
227                try {
228                    if (Boolean.FALSE.equals(task.call())) {
229                        setShuttingDown(false);
230                        return false;
231                    }
232                } catch (Exception ex) {
233                    log.error("Unable to stop", ex);
234                    setShuttingDown(false);
235                    return false;
236                }
237            }
238            // close any open windows by triggering a closing event
239            // this gives open windows a final chance to perform any cleanup
240            if (!GraphicsEnvironment.isHeadless()) {
241                Arrays.asList(Frame.getFrames()).stream().forEach(frame -> {
242                    // do not run on thread, or in parallel, as System.exit()
243                    // will get called before windows can close
244                    if (frame.isDisplayable()) { // dispose() has not been called
245                        log.debug("Closing frame \"{}\", title: \"{}\"", frame.getName(), frame.getTitle());
246                        Date timer = new Date();
247                        frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));
248                        log.debug("Frame \"{}\" took {} milliseconds to close", frame.getName(), new Date().getTime() - timer.getTime());
249                    }
250                });
251            }
252            log.debug("windows completed closing {} milliseconds after starting shutdown", new Date().getTime() - start.getTime());
253            // wait for parallel tasks to complete
254            runShutDownTasks(new HashSet<>(earlyRunnables), "JMRI ShutDown - Early Tasks");
255            runShutDownTasks(runnables, "JMRI ShutDown - Main Tasks");
256
257            // success
258            log.debug("Shutdown took {} milliseconds.", new Date().getTime() - start.getTime());
259            log.info("Normal termination complete");
260            // and now terminate forcefully
261            if (exit) {
262                System.exit(status);
263            }
264        }
265        return false;
266    }
267
268    // blocks the main Thread until tasks complete or timed out
269    private void runShutDownTasks(Set<Runnable> toRun, String threadName ) {
270        Set<Runnable> sDrunnables = new HashSet<>(toRun); // copy list so cannot be modified
271        if ( sDrunnables.isEmpty() ) {
272            return;
273        }
274        // use a custom Executor which checks the Task output for Exceptions.
275        ExecutorService executor = new ShutDownThreadPoolExecutor(sDrunnables.size(), threadName);
276        List<Future<?>> complete = new ArrayList<>();
277        long timeoutEnd = new Date().getTime()+ tasksTimeOutMilliSec;
278
279        
280        sDrunnables.forEach((runnable) -> {
281             complete.add(executor.submit(runnable));
282        });
283
284        executor.shutdown(); // no more tasks allowed from here, starts the threads.
285        while (!executor.isTerminated() && ( timeoutEnd > new Date().getTime() )) {
286            try {
287                // awaitTermination blocks the thread, checking occasionally
288                executor.awaitTermination(50, TimeUnit.MILLISECONDS);
289            } catch (InterruptedException e) {
290                // ignore
291            }
292        }
293
294        // so we're either all complete or timed out.
295        // check if a task timed out, if so log.
296        for ( Future<?> future : complete) {
297            if ( !future.isDone() ) {
298                log.error("Could not complete Shutdown Task in time: {} ", future );
299            }
300        }
301
302        executor.shutdownNow(); // do not leave Threads hanging before exit, force stop.
303
304    }
305
306    /**
307     * {@inheritDoc}
308     */
309    @Override
310    public boolean isShuttingDown() {
311        return shuttingDown;
312    }
313
314    /**
315     * This method is static so that if multiple DefaultShutDownManagers are
316     * registered, they are all aware of this state.
317     *
318     * @param state true if shutting down; false otherwise
319     */
320    protected void setShuttingDown(boolean state) {
321        boolean old = shuttingDown;
322        setStaticShuttingDown(state);
323        log.debug("Setting shuttingDown to {}", state);
324        firePropertyChange(PROP_SHUTTING_DOWN, old, state);
325    }
326
327    // package private so tests can reset
328    synchronized static void setStaticShuttingDown(boolean state){
329        shuttingDown = state;
330    }
331
332    private static class ShutDownThreadPoolExecutor extends ThreadPoolExecutor {
333
334        // use up to 8 threads for parallel tasks
335        // 10 seconds for tasks to enter a thread from queue
336        // set thread name with custom ThreadFactory
337        private ShutDownThreadPoolExecutor(int numberTasks, String threadName) {
338            super(8, 8, 10, TimeUnit.SECONDS,
339                new ArrayBlockingQueue<Runnable>(numberTasks), new ShutDownThreadFactory(threadName));
340        }
341
342        @Override
343        public void afterExecute(Runnable r, Throwable t) {
344            super.afterExecute(r, t);
345            // System.out.println("afterExecute "+ r);
346            if (t == null && r instanceof Future<?>) {
347                try {
348                    Future<?> future = (Future<?>) r;
349                    if (future.isDone()) {
350                        future.get();
351                    }
352                } catch (CancellationException ce) {
353                    t = ce;
354                } catch (ExecutionException ee) {
355                    t = ee.getCause();
356                } catch (InterruptedException ie) {
357                    Thread.currentThread().interrupt();
358                }
359            }
360            if (t != null) {
361                log.error("Issue Completing ShutdownTask : ", t);
362            }
363        }
364    }
365
366    private static class ShutDownThreadFactory implements ThreadFactory {
367        
368        private final String threadName;
369        
370        private ShutDownThreadFactory( String threadName ){
371            super();
372            this.threadName = threadName;
373        }
374        
375        @Override
376        public Thread newThread(Runnable r) {
377            return new Thread(r, threadName);
378        }
379    }
380
381    private static class EarlyTask implements Runnable {
382
383        final ShutDownTask task; // access outside of this class
384
385        EarlyTask( ShutDownTask runnableTask) {
386            task = runnableTask;
387        }
388
389        @Override
390        public void run() {
391            task.runEarly();
392        }
393
394        @Override // improve error message on failure
395        public String toString(){
396            return task.toString();
397        }
398
399    }
400
401    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultShutDownManager.class);
402
403}