View Javadoc
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 }