    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}