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}