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