    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}