    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}