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}