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}