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