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   *   http://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  package org.apache.maven.surefire.booter;
20  
21  import javax.annotation.Nonnull;
22  
23  import java.lang.reflect.Method;
24  import java.util.Optional;
25  
26  import static org.apache.maven.surefire.api.util.ReflectionUtils.invokeMethodWithArray;
27  import static org.apache.maven.surefire.api.util.ReflectionUtils.tryGetMethod;
28  import static org.apache.maven.surefire.api.util.ReflectionUtils.tryLoadClass;
29  
30  /**
31   * Checks if a process is alive using the ProcessHandle API via reflection.
32   * <p>
33   * This implementation uses reflection to access the Java 9+ {@code ProcessHandle} API,
34   * allowing the class to compile on Java 8 while functioning on Java 9+.
35   * <p>
36   * The checker detects two scenarios indicating the process is no longer available:
37   * <ol>
38   *   <li>The process has terminated ({@code ProcessHandle.isAlive()} returns {@code false})</li>
39   *   <li>The PID has been reused by the OS for a new process (start time differs from initial)</li>
40   * </ol>
41   *
42   * @since 3.5.5
43   */
44  final class ProcessHandleChecker implements ProcessChecker {
45  
46      /** Whether ProcessHandle API is available and reflection setup succeeded */
47      private static final boolean AVAILABLE;
48  
49      // Method references for ProcessHandle
50      private static final Method PROCESS_HANDLE_OF; // ProcessHandle.of(long) -> Optional<ProcessHandle>
51      private static final Method PROCESS_HANDLE_IS_ALIVE; // ProcessHandle.isAlive() -> boolean
52      private static final Method PROCESS_HANDLE_INFO; // ProcessHandle.info() -> ProcessHandle.Info
53  
54      // Method references for ProcessHandle.Info
55      private static final Method INFO_START_INSTANT; // ProcessHandle.Info.startInstant() -> Optional<Instant>
56  
57      // Method reference for Instant
58      private static final Method INSTANT_TO_EPOCH_MILLI; // Instant.toEpochMilli() -> long
59  
60      static {
61          ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
62  
63          // Load classes using ReflectionUtils
64          Class<?> processHandleClass = tryLoadClass(classLoader, "java.lang.ProcessHandle");
65          Class<?> processHandleInfoClass = tryLoadClass(classLoader, "java.lang.ProcessHandle$Info");
66          Class<?> optionalClass = tryLoadClass(classLoader, "java.util.Optional");
67          Class<?> instantClass = tryLoadClass(classLoader, "java.time.Instant");
68  
69          Method processHandleOf = null;
70          Method processHandleIsAlive = null;
71          Method processHandleInfo = null;
72          Method infoStartInstant = null;
73          Method optionalIsPresent = null;
74          Method optionalGet = null;
75          Method optionalOrElse = null;
76          Method instantToEpochMilli = null;
77  
78          if (processHandleClass != null && processHandleInfoClass != null && optionalClass != null) {
79              // ProcessHandle methods
80              processHandleOf = tryGetMethod(processHandleClass, "of", long.class);
81              processHandleIsAlive = tryGetMethod(processHandleClass, "isAlive");
82              processHandleInfo = tryGetMethod(processHandleClass, "info");
83  
84              // ProcessHandle.Info methods
85              infoStartInstant = tryGetMethod(processHandleInfoClass, "startInstant");
86  
87              // Optional methods
88              optionalIsPresent = tryGetMethod(optionalClass, "isPresent");
89              optionalGet = tryGetMethod(optionalClass, "get");
90              optionalOrElse = tryGetMethod(optionalClass, "orElse", Object.class);
91  
92              // Instant methods (for processInfo)
93              if (instantClass != null) {
94                  instantToEpochMilli = tryGetMethod(instantClass, "toEpochMilli");
95              }
96          }
97  
98          // All methods must be available for ProcessHandle API to be usable
99          AVAILABLE = processHandleOf != null
100                 && processHandleIsAlive != null
101                 && processHandleInfo != null
102                 && infoStartInstant != null
103                 && optionalIsPresent != null
104                 && optionalGet != null
105                 && optionalOrElse != null;
106 
107         PROCESS_HANDLE_OF = processHandleOf;
108         PROCESS_HANDLE_IS_ALIVE = processHandleIsAlive;
109         PROCESS_HANDLE_INFO = processHandleInfo;
110         INFO_START_INSTANT = infoStartInstant;
111         INSTANT_TO_EPOCH_MILLI = instantToEpochMilli;
112     }
113 
114     private final long pid;
115     private final Object processHandle; // ProcessHandle (stored as Object)
116     private volatile Object initialStartInstant; // Instant (stored as Object)
117     private volatile boolean stopped;
118 
119     /**
120      * Creates a new checker for the given process ID.
121      *
122      * @param pid the process ID as a string
123      * @throws NumberFormatException if pid is not a valid long
124      */
125     ProcessHandleChecker(@Nonnull String pid) {
126         this.pid = Long.parseLong(pid);
127         try {
128             Optional<?> optionalObject = (Optional<?>) PROCESS_HANDLE_OF.invoke(null, this.pid);
129             processHandle = optionalObject.orElse(null);
130             initialStartInstant = getInitialStartInstant();
131         } catch (Exception e) {
132             throw new IllegalStateException("Failed to initialize ProcessHandleChecker for PID " + pid, e);
133         }
134     }
135 
136     /**
137      * Returns whether the ProcessHandle API is available for use.
138      * This is a static check that can be used by the factory.
139      *
140      * @return true if ProcessHandle API is available (Java 9+)
141      */
142     static boolean isAvailable() {
143         return AVAILABLE;
144     }
145 
146     @Override
147     public boolean canUse() {
148         return (AVAILABLE && !stopped);
149     }
150 
151     /**
152      * {@inheritDoc}
153      * <p>
154      * This implementation checks both that the process is alive and that it's the same process
155      * that was originally identified (by comparing start times to detect PID reuse).
156      */
157     @Override
158     public boolean isProcessAlive() {
159         if (!canUse()) {
160             throw new IllegalStateException("irrelevant to call isProcessAlive()");
161         }
162 
163         try {
164             // Check if process is still running: processHandle.isAlive()
165             boolean isAlive = invokeMethodWithArray(processHandle, PROCESS_HANDLE_IS_ALIVE);
166             if (!isAlive) {
167                 return false;
168             }
169 
170             // Verify it's the same process (not a reused PID)
171             if (initialStartInstant != null) {
172                 // processHandle.info().startInstant()
173                 Object info = invokeMethodWithArray(processHandle, PROCESS_HANDLE_INFO);
174                 Optional<?> optionalInstant = invokeMethodWithArray(info, INFO_START_INSTANT);
175 
176                 if (optionalInstant.isPresent()) {
177                     Object currentStartInstant = optionalInstant.get();
178                     // PID was reused for a different process
179                     return currentStartInstant.equals(initialStartInstant);
180                 }
181             }
182 
183             return true;
184         } catch (RuntimeException e) {
185             // Reflection failed during runtime - treat as process not alive
186             return false;
187         }
188     }
189 
190     private Object getInitialStartInstant() {
191         try {
192             Object info = invokeMethodWithArray(processHandle, PROCESS_HANDLE_INFO);
193             Optional<?> optionalInstant = invokeMethodWithArray(info, INFO_START_INSTANT);
194             return optionalInstant.orElse(null);
195         } catch (RuntimeException e) {
196             return null;
197         }
198     }
199 
200     @Override
201     public void destroyActiveCommands() {
202         stopped = true;
203         // No subprocess to destroy - ProcessHandle doesn't spawn processes
204     }
205 
206     @Override
207     public boolean isStopped() {
208         return stopped;
209     }
210 
211     @Override
212     public void stop() {
213         stopped = true;
214     }
215 
216     @Override
217     public ProcessInfo processInfo() {
218         Object startInstant = getInitialStartInstant();
219         if (startInstant == null || INSTANT_TO_EPOCH_MILLI == null) {
220             return null;
221         }
222         try {
223             long startTimeMillis = invokeMethodWithArray(startInstant, INSTANT_TO_EPOCH_MILLI);
224             return ProcessInfo.processHandleInfo(String.valueOf(pid), startTimeMillis);
225         } catch (RuntimeException e) {
226             return null;
227         }
228     }
229 
230     @Override
231     public String toString() {
232         String args = "pid=" + pid + ", stopped=" + stopped + ", hasHandle=" + (processHandle != null);
233         if (initialStartInstant != null) {
234             args += ", startInstant=" + initialStartInstant;
235         }
236         return "ProcessHandleChecker{" + args + "}";
237     }
238 }