001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 */
017
018package org.apache.commons.exec;
019
020import java.util.Vector;
021
022/**
023 * Destroys all registered {@code Process}es when the VM exits.
024 */
025public class ShutdownHookProcessDestroyer implements ProcessDestroyer, Runnable {
026
027    private final class ProcessDestroyerThread extends Thread {
028
029        private boolean shouldDestroy = true;
030
031        public ProcessDestroyerThread() {
032            super("ProcessDestroyer Shutdown Hook");
033        }
034
035        @Override
036        public void run() {
037            if (shouldDestroy) {
038                ShutdownHookProcessDestroyer.this.run();
039            }
040        }
041
042        public void setShouldDestroy(final boolean shouldDestroy) {
043            this.shouldDestroy = shouldDestroy;
044        }
045    }
046
047    /** The list of currently running processes. */
048    private final Vector<Process> processes = new Vector<>();
049
050    /** The thread registered at the JVM to execute the shutdown handler. */
051    private ProcessDestroyerThread destroyProcessThread;
052
053    /** Whether or not this ProcessDestroyer has been registered as a shutdown hook. */
054    private boolean added;
055
056    /**
057     * Whether or not this ProcessDestroyer is currently running as shutdown hook.
058     */
059    private volatile boolean running;
060
061    /**
062     * Constructs a {@code ProcessDestroyer} and obtains {@code Runtime.addShutdownHook()} and {@code Runtime.removeShutdownHook()} through reflection. The
063     * ProcessDestroyer manages a list of processes to be destroyed when the VM exits. If a process is added when the list is empty, this
064     * {@code ProcessDestroyer} is registered as a shutdown hook. If removing a process results in an empty list, the {@code ProcessDestroyer} is removed as a
065     * shutdown hook.
066     */
067    public ShutdownHookProcessDestroyer() {
068    }
069
070    /**
071     * Returns {@code true} if the specified {@code Process} was successfully added to the list of processes to destroy upon VM exit.
072     *
073     * @param process the process to add.
074     * @return {@code true} if the specified {@code Process} was successfully added.
075     */
076    @Override
077    public boolean add(final Process process) {
078        synchronized (processes) {
079            // if this list is empty, register the shutdown hook
080            if (processes.isEmpty()) {
081                addShutdownHook();
082            }
083            processes.addElement(process);
084            return processes.contains(process);
085        }
086    }
087
088    /**
089     * Registers this {@code ProcessDestroyer} as a shutdown hook.
090     */
091    private void addShutdownHook() {
092        if (!running) {
093            destroyProcessThread = new ProcessDestroyerThread();
094            Runtime.getRuntime().addShutdownHook(destroyProcessThread);
095            added = true;
096        }
097    }
098
099    /**
100     * Tests whether or not the ProcessDestroyer is registered as shutdown hook.
101     *
102     * @return true if this is currently added as shutdown hook.
103     */
104    public boolean isAddedAsShutdownHook() {
105        return added;
106    }
107
108    /**
109     * Tests emptiness (size == 0).
110     *
111     * @return Whether or not this is empty.
112     * @since 1.4.0
113     */
114    public boolean isEmpty() {
115        return size() == 0;
116    }
117
118    /**
119     * Returns {@code true} if the specified {@code Process} was successfully removed from the list of processes to destroy upon VM exit.
120     *
121     * @param process the process to remove.
122     * @return {@code true} if the specified {@code Process} was successfully removed.
123     */
124    @Override
125    public boolean remove(final Process process) {
126        synchronized (processes) {
127            final boolean processRemoved = processes.removeElement(process);
128            if (processRemoved && processes.isEmpty()) {
129                removeShutdownHook();
130            }
131            return processRemoved;
132        }
133    }
134
135    /**
136     * Removes this {@code ProcessDestroyer} as a shutdown hook, uses reflection to ensure pre-JDK 1.3 compatibility
137     */
138    private void removeShutdownHook() {
139        if (added && !running) {
140            final boolean removed = Runtime.getRuntime().removeShutdownHook(destroyProcessThread);
141            if (!removed) {
142                System.err.println("Could not remove shutdown hook");
143            }
144            // start the hook thread, a unstarted thread may not be eligible for garbage collection Cf.: http://developer.java.sun.com/developer/
145            // bugParade/bugs/4533087.html
146
147            destroyProcessThread.setShouldDestroy(false);
148            destroyProcessThread.start();
149            // this should return quickly, since it basically is a NO-OP.
150            try {
151                destroyProcessThread.join(20000);
152            } catch (final InterruptedException ie) {
153                // the thread didn't die in time
154                // it should not kill any processes unexpectedly
155            }
156            destroyProcessThread = null;
157            added = false;
158        }
159    }
160
161    /**
162     * Invoked by the VM when it is exiting.
163     */
164    @Override
165    public void run() {
166        synchronized (processes) {
167            running = true;
168            processes.forEach(process -> {
169                try {
170                    process.destroy();
171                } catch (final Throwable t) {
172                    System.err.println("Unable to terminate process during process shutdown");
173                }
174            });
175        }
176    }
177
178    /**
179     * Returns the number of registered processes.
180     *
181     * @return the number of register process.
182     */
183    @Override
184    public int size() {
185        return processes.size();
186    }
187}