1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.exec;
19
20 import java.util.Vector;
21 import java.util.concurrent.atomic.AtomicBoolean;
22
23 /**
24 * Destroys all registered {@code Process}es when the VM exits.
25 */
26 public class ShutdownHookProcessDestroyer implements ProcessDestroyer, Runnable {
27
28 private final class ProcessDestroyerThread extends Thread {
29
30 private AtomicBoolean shouldDestroy = new AtomicBoolean(true);
31
32 public ProcessDestroyerThread() {
33 super("ProcessDestroyer Shutdown Hook");
34 }
35
36 @Override
37 public void run() {
38 if (shouldDestroy.get()) {
39 ShutdownHookProcessDestroyer.this.run();
40 }
41 }
42
43 public void setShouldDestroy(final boolean shouldDestroy) {
44 this.shouldDestroy.compareAndSet(!shouldDestroy, shouldDestroy);
45 }
46 }
47
48 /** The list of currently running processes. */
49 private final Vector<Process> processes = new Vector<>();
50
51 /** The thread registered at the JVM to execute the shutdown handler. */
52 private ProcessDestroyerThread destroyProcessThread;
53
54 /** Whether or not this ProcessDestroyer has been registered as a shutdown hook. */
55 private AtomicBoolean added = new AtomicBoolean();
56
57 /**
58 * Whether or not this ProcessDestroyer is currently running as shutdown hook.
59 */
60 private AtomicBoolean running = new AtomicBoolean();
61
62 /**
63 * Constructs a {@code ProcessDestroyer} and obtains {@code Runtime.addShutdownHook()} and {@code Runtime.removeShutdownHook()} through reflection. The
64 * ProcessDestroyer manages a list of processes to be destroyed when the VM exits. If a process is added when the list is empty, this
65 * {@code ProcessDestroyer} is registered as a shutdown hook. If removing a process results in an empty list, the {@code ProcessDestroyer} is removed as a
66 * shutdown hook.
67 */
68 public ShutdownHookProcessDestroyer() {
69 }
70
71 /**
72 * Returns {@code true} if the specified {@code Process} was successfully added to the list of processes to destroy upon VM exit.
73 *
74 * @param process the process to add.
75 * @return {@code true} if the specified {@code Process} was successfully added.
76 */
77 @Override
78 public boolean add(final Process process) {
79 synchronized (processes) {
80 // if this list is empty, register the shutdown hook
81 if (processes.isEmpty()) {
82 addShutdownHook();
83 }
84 processes.addElement(process);
85 return processes.contains(process);
86 }
87 }
88
89 /**
90 * Registers this {@code ProcessDestroyer} as a shutdown hook.
91 */
92 private void addShutdownHook() {
93 if (!running.get()) {
94 destroyProcessThread = new ProcessDestroyerThread();
95 Runtime.getRuntime().addShutdownHook(destroyProcessThread);
96 added.compareAndSet(false, true);
97 }
98 }
99
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 }