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 *      https://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;
021import java.util.concurrent.atomic.AtomicBoolean;
022
023/**
024 * Destroys all registered {@code Process}es when the VM exits.
025 */
026public class ShutdownHookProcessDestroyer implements ProcessDestroyer, Runnable {
027
028    private final class ProcessDestroyerThread extends Thread {
029
030        private AtomicBoolean shouldDestroy = new AtomicBoolean(true);
031
032        public ProcessDestroyerThread() {
033            super("ProcessDestroyer Shutdown Hook");
034        }
035
036        @Override
037        public void run() {
038            if (shouldDestroy.get()) {
039                ShutdownHookProcessDestroyer.this.run();
040            }
041        }
042
043        public void setShouldDestroy(final boolean shouldDestroy) {
044            this.shouldDestroy.compareAndSet(!shouldDestroy, shouldDestroy);
045        }
046    }
047
048    /** The list of currently running processes. */
049    private final Vector<Process> processes = new Vector<>();
050
051    /** The thread registered at the JVM to execute the shutdown handler. */
052    private ProcessDestroyerThread destroyProcessThread;
053
054    /** Whether or not this ProcessDestroyer has been registered as a shutdown hook. */
055    private AtomicBoolean added = new AtomicBoolean();
056
057    /**
058     * Whether or not this ProcessDestroyer is currently running as shutdown hook.
059     */
060    private AtomicBoolean running = new AtomicBoolean();
061
062    /**
063     * Constructs a {@code ProcessDestroyer} and obtains {@code Runtime.addShutdownHook()} and {@code Runtime.removeShutdownHook()} through reflection. The
064     * ProcessDestroyer manages a list of processes to be destroyed when the VM exits. If a process is added when the list is empty, this
065     * {@code ProcessDestroyer} is registered as a shutdown hook. If removing a process results in an empty list, the {@code ProcessDestroyer} is removed as a
066     * shutdown hook.
067     */
068    public ShutdownHookProcessDestroyer() {
069    }
070
071    /**
072     * Returns {@code true} if the specified {@code Process} was successfully added to the list of processes to destroy upon VM exit.
073     *
074     * @param process the process to add.
075     * @return {@code true} if the specified {@code Process} was successfully added.
076     */
077    @Override
078    public boolean add(final Process process) {
079        synchronized (processes) {
080            // if this list is empty, register the shutdown hook
081            if (processes.isEmpty()) {
082                addShutdownHook();
083            }
084            processes.addElement(process);
085            return processes.contains(process);
086        }
087    }
088
089    /**
090     * Registers this {@code ProcessDestroyer} as a shutdown hook.
091     */
092    private void addShutdownHook() {
093        if (!running.get()) {
094            destroyProcessThread = new ProcessDestroyerThread();
095            Runtime.getRuntime().addShutdownHook(destroyProcessThread);
096            added.compareAndSet(false, true);
097        }
098    }
099
100    /**
101     * Tests whether or not the ProcessDestroyer is registered as shutdown hook.
102     *
103     * @return true if this is currently added as shutdown hook.
104     */
105    public boolean isAddedAsShutdownHook() {
106        return added.get();
107    }
108
109    /**
110     * Tests emptiness (size == 0).
111     *
112     * @return Whether or not this is empty.
113     * @since 1.4.0
114     */
115    public boolean isEmpty() {
116        return size() == 0;
117    }
118
119    /**
120     * Returns {@code true} if the specified {@code Process} was successfully removed from the list of processes to destroy upon VM exit.
121     *
122     * @param process the process to remove.
123     * @return {@code true} if the specified {@code Process} was successfully removed.
124     */
125    @Override
126    public boolean remove(final Process process) {
127        synchronized (processes) {
128            final boolean processRemoved = processes.removeElement(process);
129            if (processRemoved && processes.isEmpty()) {
130                removeShutdownHook();
131            }
132            return processRemoved;
133        }
134    }
135
136    /**
137     * Removes this {@code ProcessDestroyer} as a shutdown hook.
138     */
139    private void removeShutdownHook() {
140        if (added.get() && !running.get()) {
141            final boolean removed = Runtime.getRuntime().removeShutdownHook(destroyProcessThread);
142            if (!removed) {
143                System.err.println("Could not remove shutdown hook");
144            }
145            // start the hook thread, a unstarted thread may not be eligible for garbage collection Cf.: https://developer.java.sun.com/developer/
146            // bugParade/bugs/4533087.html
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 ignore) {
153                // the thread didn't die in time
154                // it should not kill any processes unexpectedly
155            }
156            destroyProcessThread = null;
157            added.compareAndSet(true, 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.compareAndSet(false, 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}