This is an automated email from the ASF dual-hosted git repository.

olamy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git


The following commit(s) were added to refs/heads/master by this push:
     new 0b190142a Replace runing external process and parsing output with 
simple ProcessHandle if available (Java9+) (#3252)
0b190142a is described below

commit 0b190142a3df4cb3dda52825e7fedda59591cbc8
Author: Olivier Lamy <[email protected]>
AuthorDate: Tue Feb 10 19:07:57 2026 +1000

    Replace runing external process and parsing output with simple 
ProcessHandle if available (Java9+) (#3252)
    
    * Replacing runing external process and parsing output with simple 
ProcessHandle if available
    
    Signed-off-by: Olivier Lamy <[email protected]>
---
 maven-surefire-common/pom.xml                      |   1 -
 .../plugin/surefire/booterclient/Platform.java     |   9 +-
 .../src/site/apt/examples/shutdown.apt.vm          |   6 +-
 surefire-booter/pom.xml                            |   5 +
 .../apache/maven/surefire/booter/ForkedBooter.java |  12 +-
 .../apache/maven/surefire/booter/PpidChecker.java  |  27 +-
 .../maven/surefire/booter/ProcessChecker.java      | 108 ++++++
 .../surefire/booter/ProcessHandleChecker.java      | 238 ++++++++++++
 .../apache/maven/surefire/booter/ProcessInfo.java  |  11 +
 .../apache/maven/surefire/booter/SystemUtils.java  |   1 +
 .../maven/surefire/booter/PpidCheckerTest.java     | 432 ---------------------
 .../maven/surefire/booter/ProcessCheckerTest.java  | 248 ++++++++++++
 .../surefire/booter/ProcessHandleCheckerTest.java  | 205 ++++++++++
 13 files changed, 851 insertions(+), 452 deletions(-)

diff --git a/maven-surefire-common/pom.xml b/maven-surefire-common/pom.xml
index a9feb6dd9..072efcabf 100644
--- a/maven-surefire-common/pom.xml
+++ b/maven-surefire-common/pom.xml
@@ -172,7 +172,6 @@
     <dependency>
       <groupId>commons-io</groupId>
       <artifactId>commons-io</artifactId>
-      <version>2.21.0</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/Platform.java
 
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/Platform.java
index 12d19c5e8..e18803142 100644
--- 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/Platform.java
+++ 
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/Platform.java
@@ -29,7 +29,7 @@
 
 /**
  * Loads platform specifics.
- *
+ * TODO simplify or remove when Java 8 support is dropped
  * @author <a href="mailto:[email protected]";>Tibor Digana (tibor17)</a>
  * @since 2.20.1
  */
@@ -80,11 +80,6 @@ public Platform withJdkExecAttributesForTests(JdkAttributes 
jdk) {
     }
 
     private static Callable<Long> pidJob() {
-        return new Callable<Long>() {
-            @Override
-            public Long call() throws Exception {
-                return SystemUtils.pid();
-            }
-        };
+        return SystemUtils::pid;
     }
 }
diff --git a/maven-surefire-plugin/src/site/apt/examples/shutdown.apt.vm 
b/maven-surefire-plugin/src/site/apt/examples/shutdown.apt.vm
index 92f9ddc6c..93083858d 100644
--- a/maven-surefire-plugin/src/site/apt/examples/shutdown.apt.vm
+++ b/maven-surefire-plugin/src/site/apt/examples/shutdown.apt.vm
@@ -55,9 +55,13 @@ Shutdown of Forked JVM
 
    []
 
+   If Java9 is available, the start time of the process is determined by <<< 
ProcessHandle.current().info().startInstant() >>>.
+
    On Unix like systems the process' uptime is determined by native command 
<<< (/usr)/bin/ps -o etime= -p [PID] >>>.
 
-   On Windows the start time is determined using <<< wmic process where 
(ProcessId=[PID]) get CreationDate >>>
+   On Windows the start time is determined using <<< powershell -command "... 
Get-CimInstance Win32_Process ..." >>>.
+
+   []
    in the forked JVM.
 
 
diff --git a/surefire-booter/pom.xml b/surefire-booter/pom.xml
index c24bb7e36..74b055a9d 100644
--- a/surefire-booter/pom.xml
+++ b/surefire-booter/pom.xml
@@ -91,6 +91,11 @@
       <artifactId>powermock-api-mockito2</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
diff --git 
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java
 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java
index 1bcdc8b09..5067509c3 100644
--- 
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java
+++ 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java
@@ -215,7 +215,7 @@ private void closeForkChannel() {
     }
 
     private PingScheduler listenToShutdownCommands(String ppid) {
-        PpidChecker ppidChecker = ppid == null ? null : new PpidChecker(ppid);
+        ProcessChecker ppidChecker = ProcessChecker.of(ppid);
         commandReader.addShutdownListener(createExitHandler(ppidChecker));
         AtomicBoolean pingDone = new AtomicBoolean(true);
         commandReader.addNoopListener(createPingHandler(pingDone));
@@ -280,7 +280,7 @@ public void update(Command command) {
         };
     }
 
-    private CommandListener createExitHandler(final PpidChecker ppidChecker) {
+    private CommandListener createExitHandler(final ProcessChecker 
ppidChecker) {
         return new CommandListener() {
             @Override
             public void update(Command command) {
@@ -325,7 +325,7 @@ public void update(Command command) {
         };
     }
 
-    private Runnable createPingJob(final AtomicBoolean pingDone, final 
PpidChecker pluginProcessChecker) {
+    private Runnable createPingJob(final AtomicBoolean pingDone, final 
ProcessChecker pluginProcessChecker) {
         return new Runnable() {
             @Override
             public void run() {
@@ -515,7 +515,7 @@ private static void run(ForkedBooter booter, String[] args) 
{
         }
     }
 
-    private static boolean canUseNewPingMechanism(PpidChecker 
pluginProcessChecker) {
+    private static boolean canUseNewPingMechanism(ProcessChecker 
pluginProcessChecker) {
         return pluginProcessChecker != null && pluginProcessChecker.canUse();
     }
 
@@ -553,12 +553,12 @@ private static boolean isDebugging() {
     private static class PingScheduler {
         private final ScheduledExecutorService pingScheduler;
         private final ScheduledExecutorService processCheckerScheduler;
-        private final PpidChecker processChecker;
+        private final ProcessChecker processChecker;
 
         PingScheduler(
                 ScheduledExecutorService pingScheduler,
                 ScheduledExecutorService processCheckerScheduler,
-                PpidChecker processChecker) {
+                ProcessChecker processChecker) {
             this.pingScheduler = pingScheduler;
             this.processCheckerScheduler = processCheckerScheduler;
             this.processChecker = processChecker;
diff --git 
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/PpidChecker.java
 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/PpidChecker.java
index b8891e822..bfcc70d18 100644
--- 
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/PpidChecker.java
+++ 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/PpidChecker.java
@@ -57,11 +57,18 @@
 
 /**
  * Recognizes PID of Plugin process and determines lifetime.
+ * <p>
+ * This implementation uses native commands ({@code ps} on Unix, {@code 
powershell} on Windows)
+ * to check the parent process status. On Java 9+, consider using {@code 
ProcessHandleChecker}
+ * instead, which uses the Java {@code ProcessHandle} API and doesn't require 
spawning external processes.
  *
  * @author <a href="mailto:[email protected]";>Tibor Digana (tibor17)</a>
  * @since 2.20.1
+ * @see ProcessChecker
+ * @deprecated Use {@code ProcessHandleChecker} via {@link 
ProcessChecker#of(String)} instead
  */
-final class PpidChecker {
+@Deprecated
+final class PpidChecker implements ProcessChecker {
     private static final long MINUTES_TO_MILLIS = 60L * 1000L;
     // 25 chars 
https://superuser.com/questions/937380/get-creation-time-of-file-in-milliseconds/937401#937401
     private static final int WMIC_CREATION_DATE_VALUE_LENGTH = 25;
@@ -95,7 +102,8 @@ final class PpidChecker {
         this.ppid = ppid;
     }
 
-    boolean canUse() {
+    @Override
+    public boolean canUse() {
         if (isStopped()) {
             return false;
         }
@@ -111,7 +119,8 @@ boolean canUse() {
      *                               or this object has been {@link 
#destroyActiveCommands() destroyed}
      * @throws NullPointerException if extracted e-time is null
      */
-    boolean isProcessAlive() {
+    @Override
+    public boolean isProcessAlive() {
         if (!canUse()) {
             throw new IllegalStateException("irrelevant to call 
isProcessAlive()");
         }
@@ -226,14 +235,16 @@ ProcessInfo consumeLine(String line, ProcessInfo 
previousProcessInfo) throws Exc
         return reader.execute(psPath + "powershell", "-NoProfile", 
"-NonInteractive", "-Command", psCommand);
     }
 
-    void destroyActiveCommands() {
+    @Override
+    public void destroyActiveCommands() {
         stopped = true;
         for (Process p = destroyableCommands.poll(); p != null; p = 
destroyableCommands.poll()) {
             p.destroy();
         }
     }
 
-    boolean isStopped() {
+    @Override
+    public boolean isStopped() {
         return stopped;
     }
 
@@ -325,10 +336,16 @@ private static SimpleDateFormat 
createWindowsCreationDateFormat() {
         return formatter;
     }
 
+    @Override
     public void stop() {
         stopped = true;
     }
 
+    @Override
+    public ProcessInfo processInfo() {
+        return parentProcessInfo;
+    }
+
     /**
      * Reads standard output from {@link Process}.
      * <br>
diff --git 
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessChecker.java
 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessChecker.java
new file mode 100644
index 000000000..ef495eb53
--- /dev/null
+++ 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessChecker.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.surefire.booter;
+
+/**
+ * Interface for checking if a process (typically the parent Maven plugin) is 
still alive.
+ * <p>
+ * Implementations allow the forked JVM to detect when its parent Maven process
+ * has terminated, enabling cleanup and preventing orphaned processes.
+ *
+ * @since 3.5.5
+ */
+public interface ProcessChecker {
+
+    /**
+     * Creates the appropriate {@link ProcessChecker} implementation for the 
given parent PID.
+     * <p>
+     * On Java 9+, uses {@code ProcessHandleChecker} which leverages the 
{@code ProcessHandle} API.
+     * On Java 8, falls back to {@link PpidChecker} which uses native commands.
+     *
+     * @param ppid the parent process ID as a string, or {@code null}
+     * @return a new checker instance, or {@code null} if ppid is {@code null}
+     */
+    static ProcessChecker of(String ppid) {
+        if (ppid == null) {
+            return null;
+        }
+        if (ProcessHandleChecker.isAvailable()) {
+            return new ProcessHandleChecker(ppid);
+        }
+        return new PpidChecker(ppid);
+    }
+
+    /**
+     * Returns whether the ProcessHandle API is available in the current JVM.
+     *
+     * @return {@code true} if running on Java 9+ with ProcessHandle available
+     */
+    static boolean isProcessHandleSupported() {
+        return ProcessHandleChecker.isAvailable();
+    }
+
+    /**
+     * Checks whether this checker can be used to monitor the process.
+     * <p>
+     * This method must return {@code true} before {@link #isProcessAlive()} 
can be called.
+     * @deprecated with using ProcessHandleChecker on Java 9+, this method 
will always return {@code true} and can be removed in a future release.
+     * @return {@code true} if the checker is operational and can monitor the 
process
+     */
+    @Deprecated
+    boolean canUse();
+
+    /**
+     * Checks if the process is still alive.
+     * <p>
+     * This method can only be called after {@link #canUse()} has returned 
{@code true}.
+     *
+     * @return {@code true} if the process is still running; {@code false} if 
it has terminated
+     *         or if the PID has been reused by a different process
+     * @throws IllegalStateException if {@link #canUse()} returns {@code 
false} or if the checker
+     *                               has been stopped
+     */
+    boolean isProcessAlive();
+
+    /**
+     * Stops the checker and releases any resources.
+     * <p>
+     * After calling this method, {@link #canUse()} will return {@code false}.
+     */
+    void stop();
+
+    /**
+     * Destroys any active commands or subprocesses used by this checker.
+     * <p>
+     * This is called during shutdown to ensure clean termination.
+     */
+    void destroyActiveCommands();
+
+    /**
+     * Checks if the checker has been stopped.
+     *
+     * @return {@code true} if {@link #stop()} or {@link 
#destroyActiveCommands()} has been called
+     */
+    boolean isStopped();
+
+    /**
+     * Returns information about the process being checked.
+     *
+     * @return the process information, or {@code null} if not yet initialized
+     */
+    ProcessInfo processInfo();
+}
diff --git 
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessHandleChecker.java
 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessHandleChecker.java
new file mode 100644
index 000000000..a97e6ace4
--- /dev/null
+++ 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessHandleChecker.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.surefire.booter;
+
+import javax.annotation.Nonnull;
+
+import java.lang.reflect.Method;
+import java.util.Optional;
+
+import static 
org.apache.maven.surefire.api.util.ReflectionUtils.invokeMethodWithArray;
+import static org.apache.maven.surefire.api.util.ReflectionUtils.tryGetMethod;
+import static org.apache.maven.surefire.api.util.ReflectionUtils.tryLoadClass;
+
+/**
+ * Checks if a process is alive using the ProcessHandle API via reflection.
+ * <p>
+ * This implementation uses reflection to access the Java 9+ {@code 
ProcessHandle} API,
+ * allowing the class to compile on Java 8 while functioning on Java 9+.
+ * <p>
+ * The checker detects two scenarios indicating the process is no longer 
available:
+ * <ol>
+ *   <li>The process has terminated ({@code ProcessHandle.isAlive()} returns 
{@code false})</li>
+ *   <li>The PID has been reused by the OS for a new process (start time 
differs from initial)</li>
+ * </ol>
+ *
+ * @since 3.5.5
+ */
+final class ProcessHandleChecker implements ProcessChecker {
+
+    /** Whether ProcessHandle API is available and reflection setup succeeded 
*/
+    private static final boolean AVAILABLE;
+
+    // Method references for ProcessHandle
+    private static final Method PROCESS_HANDLE_OF; // ProcessHandle.of(long) 
-> Optional<ProcessHandle>
+    private static final Method PROCESS_HANDLE_IS_ALIVE; // 
ProcessHandle.isAlive() -> boolean
+    private static final Method PROCESS_HANDLE_INFO; // ProcessHandle.info() 
-> ProcessHandle.Info
+
+    // Method references for ProcessHandle.Info
+    private static final Method INFO_START_INSTANT; // 
ProcessHandle.Info.startInstant() -> Optional<Instant>
+
+    // Method reference for Instant
+    private static final Method INSTANT_TO_EPOCH_MILLI; // 
Instant.toEpochMilli() -> long
+
+    static {
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+
+        // Load classes using ReflectionUtils
+        Class<?> processHandleClass = tryLoadClass(classLoader, 
"java.lang.ProcessHandle");
+        Class<?> processHandleInfoClass = tryLoadClass(classLoader, 
"java.lang.ProcessHandle$Info");
+        Class<?> optionalClass = tryLoadClass(classLoader, 
"java.util.Optional");
+        Class<?> instantClass = tryLoadClass(classLoader, "java.time.Instant");
+
+        Method processHandleOf = null;
+        Method processHandleIsAlive = null;
+        Method processHandleInfo = null;
+        Method infoStartInstant = null;
+        Method optionalIsPresent = null;
+        Method optionalGet = null;
+        Method optionalOrElse = null;
+        Method instantToEpochMilli = null;
+
+        if (processHandleClass != null && processHandleInfoClass != null && 
optionalClass != null) {
+            // ProcessHandle methods
+            processHandleOf = tryGetMethod(processHandleClass, "of", 
long.class);
+            processHandleIsAlive = tryGetMethod(processHandleClass, "isAlive");
+            processHandleInfo = tryGetMethod(processHandleClass, "info");
+
+            // ProcessHandle.Info methods
+            infoStartInstant = tryGetMethod(processHandleInfoClass, 
"startInstant");
+
+            // Optional methods
+            optionalIsPresent = tryGetMethod(optionalClass, "isPresent");
+            optionalGet = tryGetMethod(optionalClass, "get");
+            optionalOrElse = tryGetMethod(optionalClass, "orElse", 
Object.class);
+
+            // Instant methods (for processInfo)
+            if (instantClass != null) {
+                instantToEpochMilli = tryGetMethod(instantClass, 
"toEpochMilli");
+            }
+        }
+
+        // All methods must be available for ProcessHandle API to be usable
+        AVAILABLE = processHandleOf != null
+                && processHandleIsAlive != null
+                && processHandleInfo != null
+                && infoStartInstant != null
+                && optionalIsPresent != null
+                && optionalGet != null
+                && optionalOrElse != null;
+
+        PROCESS_HANDLE_OF = processHandleOf;
+        PROCESS_HANDLE_IS_ALIVE = processHandleIsAlive;
+        PROCESS_HANDLE_INFO = processHandleInfo;
+        INFO_START_INSTANT = infoStartInstant;
+        INSTANT_TO_EPOCH_MILLI = instantToEpochMilli;
+    }
+
+    private final long pid;
+    private final Object processHandle; // ProcessHandle (stored as Object)
+    private volatile Object initialStartInstant; // Instant (stored as Object)
+    private volatile boolean stopped;
+
+    /**
+     * Creates a new checker for the given process ID.
+     *
+     * @param pid the process ID as a string
+     * @throws NumberFormatException if pid is not a valid long
+     */
+    ProcessHandleChecker(@Nonnull String pid) {
+        this.pid = Long.parseLong(pid);
+        try {
+            Optional<?> optionalObject = (Optional<?>) 
PROCESS_HANDLE_OF.invoke(null, this.pid);
+            processHandle = optionalObject.orElse(null);
+            initialStartInstant = getInitialStartInstant();
+        } catch (Exception e) {
+            throw new IllegalStateException("Failed to initialize 
ProcessHandleChecker for PID " + pid, e);
+        }
+    }
+
+    /**
+     * Returns whether the ProcessHandle API is available for use.
+     * This is a static check that can be used by the factory.
+     *
+     * @return true if ProcessHandle API is available (Java 9+)
+     */
+    static boolean isAvailable() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public boolean canUse() {
+        return (AVAILABLE && !stopped);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This implementation checks both that the process is alive and that it's 
the same process
+     * that was originally identified (by comparing start times to detect PID 
reuse).
+     */
+    @Override
+    public boolean isProcessAlive() {
+        if (!canUse()) {
+            throw new IllegalStateException("irrelevant to call 
isProcessAlive()");
+        }
+
+        try {
+            // Check if process is still running: processHandle.isAlive()
+            boolean isAlive = invokeMethodWithArray(processHandle, 
PROCESS_HANDLE_IS_ALIVE);
+            if (!isAlive) {
+                return false;
+            }
+
+            // Verify it's the same process (not a reused PID)
+            if (initialStartInstant != null) {
+                // processHandle.info().startInstant()
+                Object info = invokeMethodWithArray(processHandle, 
PROCESS_HANDLE_INFO);
+                Optional<?> optionalInstant = invokeMethodWithArray(info, 
INFO_START_INSTANT);
+
+                if (optionalInstant.isPresent()) {
+                    Object currentStartInstant = optionalInstant.get();
+                    // PID was reused for a different process
+                    return currentStartInstant.equals(initialStartInstant);
+                }
+            }
+
+            return true;
+        } catch (RuntimeException e) {
+            // Reflection failed during runtime - treat as process not alive
+            return false;
+        }
+    }
+
+    private Object getInitialStartInstant() {
+        try {
+            Object info = invokeMethodWithArray(processHandle, 
PROCESS_HANDLE_INFO);
+            Optional<?> optionalInstant = invokeMethodWithArray(info, 
INFO_START_INSTANT);
+            return optionalInstant.orElse(null);
+        } catch (RuntimeException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public void destroyActiveCommands() {
+        stopped = true;
+        // No subprocess to destroy - ProcessHandle doesn't spawn processes
+    }
+
+    @Override
+    public boolean isStopped() {
+        return stopped;
+    }
+
+    @Override
+    public void stop() {
+        stopped = true;
+    }
+
+    @Override
+    public ProcessInfo processInfo() {
+        Object startInstant = getInitialStartInstant();
+        if (startInstant == null || INSTANT_TO_EPOCH_MILLI == null) {
+            return null;
+        }
+        try {
+            long startTimeMillis = invokeMethodWithArray(startInstant, 
INSTANT_TO_EPOCH_MILLI);
+            return ProcessInfo.processHandleInfo(String.valueOf(pid), 
startTimeMillis);
+        } catch (RuntimeException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public String toString() {
+        String args = "pid=" + pid + ", stopped=" + stopped + ", hasHandle=" + 
(processHandle != null);
+        if (initialStartInstant != null) {
+            args += ", startInstant=" + initialStartInstant;
+        }
+        return "ProcessHandleChecker{" + args + "}";
+    }
+}
diff --git 
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessInfo.java
 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessInfo.java
index 771445965..630c7dd5b 100644
--- 
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessInfo.java
+++ 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessInfo.java
@@ -48,6 +48,17 @@ final class ProcessInfo {
         return new ProcessInfo(pid, startTimestamp);
     }
 
+    /**
+     * Creates process info from ProcessHandle API data.
+     *
+     * @param pid the process ID
+     * @param startTimeMillis the process start time in epoch milliseconds
+     * @return a new ProcessInfo instance
+     */
+    static @Nonnull ProcessInfo processHandleInfo(String pid, long 
startTimeMillis) {
+        return new ProcessInfo(pid, startTimeMillis);
+    }
+
     private final String pid;
     private final long time;
 
diff --git 
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/SystemUtils.java
 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/SystemUtils.java
index 3f7b4aa5b..35f5cf75e 100644
--- 
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/SystemUtils.java
+++ 
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/SystemUtils.java
@@ -172,6 +172,7 @@ public static ClassLoader platformClassLoader() {
         return null;
     }
 
+    // TODO simplify or remove when Java 8 support is dropped
     public static Long pid() {
         if (isBuiltInJava9AtLeast()) {
             Long pid = pidOnJava9();
diff --git 
a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/PpidCheckerTest.java
 
b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/PpidCheckerTest.java
deleted file mode 100644
index 5b05d1a46..000000000
--- 
a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/PpidCheckerTest.java
+++ /dev/null
@@ -1,432 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.surefire.booter;
-
-import javax.annotation.Nonnull;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InterruptedIOException;
-import java.lang.management.ManagementFactory;
-import java.util.Random;
-import java.util.regex.Matcher;
-
-import org.apache.maven.surefire.api.booter.DumpErrorSingleton;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import static java.nio.file.Files.readAllBytes;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.apache.maven.surefire.booter.ProcessInfo.unixProcessInfo;
-import static org.apache.maven.surefire.booter.ProcessInfo.windowsProcessInfo;
-import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_UNIX;
-import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_WINDOWS;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeThat;
-import static org.junit.Assume.assumeTrue;
-import static org.powermock.reflect.Whitebox.invokeMethod;
-import static org.powermock.reflect.Whitebox.setInternalState;
-
-/**
- * Testing {@link PpidChecker} on a platform.
- *
- * @author <a href="mailto:[email protected]";>Tibor Digana (tibor17)</a>
- * @since 2.20.1
- */
-@SuppressWarnings("checkstyle:magicnumber")
-public class PpidCheckerTest {
-    private static final Random RND = new Random();
-
-    @Rule
-    public final ExpectedException exceptions = ExpectedException.none();
-
-    @Rule
-    public final TemporaryFolder tempFolder = new TemporaryFolder();
-
-    private File reportsDir;
-    private String dumpFileName;
-
-    @Before
-    public void initTmpFile() {
-        reportsDir = tempFolder.getRoot();
-        dumpFileName = "surefire-" + RND.nextLong();
-    }
-
-    @After
-    public void deleteTmpFiles() {
-        tempFolder.delete();
-    }
-
-    @Test
-    public void canExecuteUnixPs() {
-        assumeTrue(IS_OS_UNIX);
-        assertThat(PpidChecker.canExecuteUnixPs())
-                .as("Surefire should be tested on real box OS, e.g. Ubuntu or 
FreeBSD.")
-                .isTrue();
-    }
-
-    @Test
-    public void shouldHavePidAtBegin() {
-        String expectedPid =
-                
ManagementFactory.getRuntimeMXBean().getName().split("@")[0].trim();
-
-        PpidChecker checker = new PpidChecker(expectedPid);
-        ProcessInfo processInfo = IS_OS_UNIX ? checker.unix() : 
checker.windows();
-
-        assertThat(processInfo).isNotNull();
-
-        assertThat(checker.canUse()).isTrue();
-
-        assertThat(checker.isProcessAlive()).isTrue();
-
-        assertThat(processInfo.getPID()).isEqualTo(expectedPid);
-
-        assertThat(processInfo.getTime()).isGreaterThan(0L);
-    }
-
-    @Test
-    public void shouldHavePid() throws Exception {
-        String expectedPid =
-                
ManagementFactory.getRuntimeMXBean().getName().split("@")[0].trim();
-
-        PpidChecker checker = new PpidChecker(expectedPid);
-        setInternalState(
-                checker,
-                "parentProcessInfo",
-                IS_OS_UNIX
-                        ? unixProcessInfo(expectedPid, 0L)
-                        : windowsProcessInfo(expectedPid, 
windowsProcessStartTime(checker)));
-
-        // the etime in Unix is measured in seconds. So let's wait 1 s at 
least.
-        SECONDS.sleep(1L);
-
-        ProcessInfo processInfo = IS_OS_UNIX ? checker.unix() : 
checker.windows();
-
-        assertThat(processInfo).isNotNull();
-
-        assertThat(checker.canUse()).isTrue();
-
-        assertThat(checker.isProcessAlive()).isTrue();
-
-        assertThat(processInfo.getPID()).isEqualTo(expectedPid);
-
-        assertThat(processInfo.getTime()).isGreaterThan(0L);
-
-        assertThat(checker.toString())
-                .contains("ppid=" + expectedPid)
-                .contains("stopped=false")
-                .contains("invalid=false")
-                .contains("error=false");
-
-        checker.destroyActiveCommands();
-        assertThat(checker.canUse()).isFalse();
-        assertThat((boolean) invokeMethod(checker, "isStopped")).isTrue();
-    }
-
-    @Test
-    public void shouldBeStopped() {
-        PpidChecker checker = new PpidChecker("0");
-        checker.stop();
-
-        assertThat(checker.canUse()).isFalse();
-
-        exceptions.expect(IllegalStateException.class);
-        exceptions.expectMessage("irrelevant to call isProcessAlive()");
-
-        checker.isProcessAlive();
-
-        fail("this test should throw exception");
-    }
-
-    @Test
-    public void shouldBeStoppedCheckerWithError() throws Exception {
-        String expectedPid =
-                
ManagementFactory.getRuntimeMXBean().getName().split("@")[0].trim();
-        DumpErrorSingleton.getSingleton().init(reportsDir, dumpFileName);
-
-        PpidChecker checker = new PpidChecker(expectedPid);
-        checker.stop();
-
-        ProcessInfo processInfo = IS_OS_UNIX ? checker.unix() : 
checker.windows();
-        assertThat(processInfo.isError()).isTrue();
-
-        String error = new String(readAllBytes(new File(reportsDir, 
dumpFileName + ".dump").toPath()));
-
-        assertThat(error).contains("<<exit>> <<0>>").contains("<<stopped>> 
<<true>>");
-    }
-
-    @Test
-    public void shouldBeEmptyDump() throws Exception {
-        String expectedPid =
-                
ManagementFactory.getRuntimeMXBean().getName().split("@")[0].trim();
-        DumpErrorSingleton.getSingleton().init(reportsDir, dumpFileName);
-
-        PpidChecker checker = new PpidChecker(expectedPid);
-
-        try {
-            Thread.currentThread().interrupt();
-
-            ProcessInfo processInfo = IS_OS_UNIX ? checker.unix() : 
checker.windows();
-            //noinspection ResultOfMethodCallIgnored
-            Thread.interrupted();
-            assertThat(processInfo.isError()).isTrue();
-
-            File dumpFile = new File(reportsDir, dumpFileName + ".dump");
-            if (dumpFile.exists()) {
-                String error = new String(readAllBytes(dumpFile.toPath()));
-
-                assertThat(error).contains("<<exit>>").contains("<<stopped>> 
<<false>>");
-            }
-        } finally {
-            //noinspection ResultOfMethodCallIgnored
-            Thread.interrupted();
-        }
-    }
-
-    @Test
-    public void shouldStartedProcessThrowInterruptedException() throws 
Exception {
-        String expectedPid =
-                
ManagementFactory.getRuntimeMXBean().getName().split("@")[0].trim();
-        DumpErrorSingleton.getSingleton().init(reportsDir, dumpFileName);
-
-        PpidChecker checker = new PpidChecker(expectedPid);
-
-        PpidChecker.ProcessInfoConsumer consumer = checker.new 
ProcessInfoConsumer(US_ASCII.name()) {
-            @Nonnull
-            @Override
-            ProcessInfo consumeLine(String line, ProcessInfo 
previousProcessInfo) throws Exception {
-                throw new InterruptedException();
-            }
-        };
-
-        String[] cmd =
-                IS_OS_WINDOWS ? new String[] {"CMD", "/A", "/X", "/C", "dir"} 
: new String[] {"/bin/sh", "-c", "ls"};
-
-        assertThat(consumer.execute(cmd).isError()).isTrue();
-        assertThat(new File(reportsDir, dumpFileName + 
".dump")).doesNotExist();
-    }
-
-    @Test
-    public void shouldStartedProcessThrowInterruptedIOException() throws 
Exception {
-        String expectedPid =
-                
ManagementFactory.getRuntimeMXBean().getName().split("@")[0].trim();
-        DumpErrorSingleton.getSingleton().init(reportsDir, dumpFileName);
-
-        PpidChecker checker = new PpidChecker(expectedPid);
-
-        PpidChecker.ProcessInfoConsumer consumer = checker.new 
ProcessInfoConsumer(US_ASCII.name()) {
-            @Nonnull
-            @Override
-            ProcessInfo consumeLine(String line, ProcessInfo 
previousProcessInfo) throws Exception {
-                throw new InterruptedIOException();
-            }
-        };
-
-        String[] cmd =
-                IS_OS_WINDOWS ? new String[] {"CMD", "/A", "/X", "/C", "dir"} 
: new String[] {"/bin/sh", "-c", "ls"};
-
-        assertThat(consumer.execute(cmd).isError()).isTrue();
-        assertThat(new File(reportsDir, dumpFileName + 
".dump")).doesNotExist();
-    }
-
-    @Test
-    public void shouldStartedProcessThrowIOException() throws Exception {
-        String expectedPid =
-                
ManagementFactory.getRuntimeMXBean().getName().split("@")[0].trim();
-        DumpErrorSingleton.getSingleton().init(reportsDir, dumpFileName);
-
-        PpidChecker checker = new PpidChecker(expectedPid);
-
-        PpidChecker.ProcessInfoConsumer consumer = checker.new 
ProcessInfoConsumer(US_ASCII.name()) {
-            @Nonnull
-            @Override
-            ProcessInfo consumeLine(String line, ProcessInfo 
previousProcessInfo) throws Exception {
-                throw new IOException("wrong command");
-            }
-        };
-
-        String[] cmd =
-                IS_OS_WINDOWS ? new String[] {"CMD", "/A", "/X", "/C", "dir"} 
: new String[] {"/bin/sh", "-c", "ls"};
-
-        assertThat(consumer.execute(cmd).isError()).isTrue();
-
-        File dumpFile = new File(reportsDir, dumpFileName + ".dump");
-
-        String error = new String(readAllBytes(dumpFile.toPath()));
-
-        
assertThat(error).contains(IOException.class.getName()).contains("wrong 
command");
-    }
-
-    @Test
-    public void shouldNotFindSuchPID() {
-        PpidChecker checker = new PpidChecker("1000000");
-        setInternalState(checker, "parentProcessInfo", 
ProcessInfo.ERR_PROCESS_INFO);
-
-        assertThat(checker.canUse()).isFalse();
-
-        exceptions.expect(IllegalStateException.class);
-        exceptions.expectMessage("irrelevant to call isProcessAlive()");
-
-        checker.isProcessAlive();
-
-        fail("this test should throw exception");
-    }
-
-    @Test
-    public void shouldNotBeAlive() {
-        PpidChecker checker = new PpidChecker("1000000");
-
-        assertThat(checker.canUse()).isTrue();
-
-        assertThat(checker.isProcessAlive()).isFalse();
-    }
-
-    @Test
-    public void shouldParseEtime() {
-        Matcher m = PpidChecker.UNIX_CMD_OUT_PATTERN.matcher("38 1234567890");
-        assertThat(m.matches()).isFalse();
-
-        m = PpidChecker.UNIX_CMD_OUT_PATTERN.matcher("05:38 1234567890");
-        assertThat(m.matches()).isTrue();
-        assertThat(PpidChecker.fromDays(m)).isEqualTo(0L);
-        assertThat(PpidChecker.fromHours(m)).isEqualTo(0L);
-        assertThat(PpidChecker.fromMinutes(m)).isEqualTo(300L);
-        assertThat(PpidChecker.fromSeconds(m)).isEqualTo(38L);
-        assertThat(PpidChecker.fromPID(m)).isEqualTo("1234567890");
-
-        m = PpidChecker.UNIX_CMD_OUT_PATTERN.matcher("00:05:38 1234567890");
-        assertThat(m.matches()).isTrue();
-        assertThat(PpidChecker.fromDays(m)).isEqualTo(0L);
-        assertThat(PpidChecker.fromHours(m)).isEqualTo(0L);
-        assertThat(PpidChecker.fromMinutes(m)).isEqualTo(300L);
-        assertThat(PpidChecker.fromSeconds(m)).isEqualTo(38L);
-        assertThat(PpidChecker.fromPID(m)).isEqualTo("1234567890");
-
-        m = PpidChecker.UNIX_CMD_OUT_PATTERN.matcher("01:05:38 1234567890");
-        assertThat(m.matches()).isTrue();
-        assertThat(PpidChecker.fromDays(m)).isEqualTo(0L);
-        assertThat(PpidChecker.fromHours(m)).isEqualTo(3600L);
-        assertThat(PpidChecker.fromMinutes(m)).isEqualTo(300L);
-        assertThat(PpidChecker.fromSeconds(m)).isEqualTo(38L);
-        assertThat(PpidChecker.fromPID(m)).isEqualTo("1234567890");
-
-        m = PpidChecker.UNIX_CMD_OUT_PATTERN.matcher("02-01:05:38 1234567890");
-        assertThat(m.matches()).isTrue();
-        assertThat(PpidChecker.fromDays(m)).isEqualTo(2 * 24 * 3600L);
-        assertThat(PpidChecker.fromHours(m)).isEqualTo(3600L);
-        assertThat(PpidChecker.fromMinutes(m)).isEqualTo(300L);
-        assertThat(PpidChecker.fromSeconds(m)).isEqualTo(38L);
-        assertThat(PpidChecker.fromPID(m)).isEqualTo("1234567890");
-
-        m = PpidChecker.UNIX_CMD_OUT_PATTERN.matcher("02-1:5:3 1234567890");
-        assertThat(m.matches()).isTrue();
-        assertThat(PpidChecker.fromDays(m)).isEqualTo(2 * 24 * 3600L);
-        assertThat(PpidChecker.fromHours(m)).isEqualTo(3600L);
-        assertThat(PpidChecker.fromMinutes(m)).isEqualTo(300L);
-        assertThat(PpidChecker.fromSeconds(m)).isEqualTo(3L);
-        assertThat(PpidChecker.fromPID(m)).isEqualTo("1234567890");
-    }
-
-    @Test
-    public void shouldParseBusyboxHoursEtime() {
-        Matcher m = PpidChecker.BUSYBOX_CMD_OUT_PATTERN.matcher("38 
1234567890");
-        assertThat(m.matches()).isFalse();
-
-        m = PpidChecker.BUSYBOX_CMD_OUT_PATTERN.matcher("05h38 1234567890");
-        assertThat(m.matches()).isTrue();
-        assertThat(PpidChecker.fromBusyboxHours(m)).isEqualTo(3600 * 5L);
-        assertThat(PpidChecker.fromBusyboxMinutes(m)).isEqualTo(60 * 38L);
-        assertThat(PpidChecker.fromBusyboxPID(m)).isEqualTo("1234567890");
-    }
-
-    @Test
-    public void shouldHaveSystemPathToPowerShellOnWindows() throws Exception {
-        assumeTrue(IS_OS_WINDOWS);
-        assumeThat(System.getenv("SystemRoot"), is(notNullValue()));
-        assumeThat(System.getenv("SystemRoot"), is(not("")));
-        assumeTrue(new File(System.getenv("SystemRoot"), 
"System32\\WindowsPowerShell\\v1.0").isDirectory());
-        assumeTrue(new File(System.getenv("SystemRoot"), 
"System32\\WindowsPowerShell\\v1.0\\powershell.exe").isFile());
-        assertThat((Boolean) invokeMethod(PpidChecker.class, 
"hasPowerShellStandardSystemPath"))
-                .isTrue();
-        assertThat(new File(System.getenv("SystemRoot"), 
"System32\\WindowsPowerShell\\v1.0\\powershell.exe"))
-                .isFile();
-    }
-
-    @Test
-    public void shouldBeTypeNull() {
-        assertThat(ProcessCheckerType.toEnum(null)).isNull();
-
-        assertThat(ProcessCheckerType.toEnum("   ")).isNull();
-
-        assertThat(ProcessCheckerType.isValid(null)).isTrue();
-    }
-
-    @Test
-    public void shouldBeException() {
-        exceptions.expect(IllegalArgumentException.class);
-        exceptions.expectMessage("unknown process checker");
-
-        assertThat(ProcessCheckerType.toEnum("anything else")).isNull();
-    }
-
-    @Test
-    public void shouldNotBeValid() {
-        assertThat(ProcessCheckerType.isValid("anything")).isFalse();
-    }
-
-    @Test
-    public void shouldBeTypePing() {
-        
assertThat(ProcessCheckerType.toEnum("ping")).isEqualTo(ProcessCheckerType.PING);
-
-        assertThat(ProcessCheckerType.isValid("ping")).isTrue();
-
-        assertThat(ProcessCheckerType.PING.getType()).isEqualTo("ping");
-    }
-
-    @Test
-    public void shouldBeTypeNative() {
-        
assertThat(ProcessCheckerType.toEnum("native")).isEqualTo(ProcessCheckerType.NATIVE);
-
-        assertThat(ProcessCheckerType.isValid("native")).isTrue();
-
-        assertThat(ProcessCheckerType.NATIVE.getType()).isEqualTo("native");
-    }
-
-    @Test
-    public void shouldBeTypeAll() {
-        
assertThat(ProcessCheckerType.toEnum("all")).isEqualTo(ProcessCheckerType.ALL);
-
-        assertThat(ProcessCheckerType.isValid("all")).isTrue();
-
-        assertThat(ProcessCheckerType.ALL.getType()).isEqualTo("all");
-    }
-
-    private static long windowsProcessStartTime(PpidChecker checker) {
-        return checker.windows().getTime();
-    }
-}
diff --git 
a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ProcessCheckerTest.java
 
b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ProcessCheckerTest.java
new file mode 100644
index 000000000..ac5883d17
--- /dev/null
+++ 
b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ProcessCheckerTest.java
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.surefire.booter;
+
+import java.lang.management.ManagementFactory;
+
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.fail;
+
+/**
+ * Testing {@link ProcessChecker} via {@link ProcessChecker#of(String)}.
+ *
+ * @since 2.20.1
+ */
+@SuppressWarnings("checkstyle:magicnumber")
+public class ProcessCheckerTest {
+
+    @Rule
+    public final ExpectedException exceptions = ExpectedException.none();
+
+    @Test
+    public void shouldHavePidAtBegin() {
+        String expectedPid =
+                
ManagementFactory.getRuntimeMXBean().getName().split("@")[0].trim();
+
+        ProcessChecker checker = ProcessChecker.of(expectedPid);
+
+        assertThat(checker).isNotNull();
+
+        assertThat(checker.canUse()).isTrue();
+
+        assertThat(checker.isProcessAlive()).isTrue();
+
+        ProcessInfo processInfo = checker.processInfo();
+        assertThat(processInfo).isNotNull();
+        assertThat(processInfo.getPID()).isEqualTo(expectedPid);
+        assertThat(processInfo.getTime()).isGreaterThan(0L);
+    }
+
+    @Test
+    public void shouldBeStopped() {
+        ProcessChecker checker = ProcessChecker.of("0");
+        checker.stop();
+
+        assertThat(checker.canUse()).isFalse();
+
+        exceptions.expect(IllegalStateException.class);
+        exceptions.expectMessage("irrelevant to call isProcessAlive()");
+
+        checker.isProcessAlive();
+
+        fail("this test should throw exception");
+    }
+
+    @Test
+    public void exceptionCallIsProcessAlive() {
+        // FIXME DisabledOnJre when we migrate to junit5 and run on unix too
+        // winddows java 8 must depends on wwmc something available
+        double v = 
Double.parseDouble(System.getProperty("java.specification.version"));
+        Assume.assumeTrue(v >= 9.0);
+        ProcessChecker checker = 
ProcessChecker.of(Long.toString(Integer.MAX_VALUE));
+        checker.stop();
+        
assertThatThrownBy(checker::isProcessAlive).isInstanceOf(IllegalStateException.class);
+    }
+
+    @Test
+    public void shouldReturnNullForNullPpid() {
+        ProcessChecker checker = ProcessChecker.of(null);
+        assertThat(checker).isNull();
+    }
+
+    @Test
+    public void shouldBeTypeNull() {
+        assertThat(ProcessCheckerType.toEnum(null)).isNull();
+
+        assertThat(ProcessCheckerType.toEnum("   ")).isNull();
+
+        assertThat(ProcessCheckerType.isValid(null)).isTrue();
+    }
+
+    @Test
+    public void shouldBeException() {
+        exceptions.expect(IllegalArgumentException.class);
+        exceptions.expectMessage("unknown process checker");
+
+        assertThat(ProcessCheckerType.toEnum("anything else")).isNull();
+    }
+
+    @Test
+    public void shouldNotBeValid() {
+        assertThat(ProcessCheckerType.isValid("anything")).isFalse();
+    }
+
+    @Test
+    public void shouldBeTypePing() {
+        
assertThat(ProcessCheckerType.toEnum("ping")).isEqualTo(ProcessCheckerType.PING);
+
+        assertThat(ProcessCheckerType.isValid("ping")).isTrue();
+
+        assertThat(ProcessCheckerType.PING.getType()).isEqualTo("ping");
+    }
+
+    @Test
+    public void shouldBeTypeNative() {
+        
assertThat(ProcessCheckerType.toEnum("native")).isEqualTo(ProcessCheckerType.NATIVE);
+
+        assertThat(ProcessCheckerType.isValid("native")).isTrue();
+
+        assertThat(ProcessCheckerType.NATIVE.getType()).isEqualTo("native");
+    }
+
+    @Test
+    public void shouldBeTypeAll() {
+        
assertThat(ProcessCheckerType.toEnum("all")).isEqualTo(ProcessCheckerType.ALL);
+
+        assertThat(ProcessCheckerType.isValid("all")).isTrue();
+
+        assertThat(ProcessCheckerType.ALL.getType()).isEqualTo("all");
+    }
+
+    @Test
+    public void shouldCreateCheckerForCurrentProcess() {
+        // Get current process PID using reflection to stay Java 8 compatible
+        String currentPid = getCurrentPid();
+        if (currentPid == null) {
+            // Skip test if we can't get PID
+            return;
+        }
+
+        ProcessChecker checker = ProcessChecker.of(currentPid);
+
+        assertThat(checker).isNotNull();
+        assertThat(checker.canUse()).isTrue();
+        assertThat(checker.isProcessAlive()).isTrue();
+        assertThat(checker.isStopped()).isFalse();
+    }
+
+    @Test
+    public void shouldSelectProcessHandleCheckerOnJava9Plus() {
+        if (!ProcessChecker.isProcessHandleSupported()) {
+            // Skip test if ProcessHandle is not available (Java 8)
+            return;
+        }
+
+        String currentPid = getCurrentPid();
+        if (currentPid == null) {
+            return;
+        }
+
+        ProcessChecker checker = ProcessChecker.of(currentPid);
+        
assertThat(checker.getClass().getSimpleName()).isEqualTo("ProcessHandleChecker");
+    }
+
+    @Test
+    public void shouldStopChecker() {
+        String currentPid = getCurrentPid();
+        if (currentPid == null) {
+            return;
+        }
+
+        ProcessChecker checker = ProcessChecker.of(currentPid);
+
+        assertThat(checker.canUse()).isTrue();
+        assertThat(checker.isStopped()).isFalse();
+
+        checker.stop();
+
+        assertThat(checker.isStopped()).isTrue();
+        assertThat(checker.canUse()).isFalse();
+    }
+
+    @Test
+    public void shouldDestroyActiveCommands() {
+        String currentPid = getCurrentPid();
+        if (currentPid == null) {
+            return;
+        }
+
+        ProcessChecker checker = ProcessChecker.of(currentPid);
+        assertThat(checker.canUse()).isTrue();
+
+        checker.destroyActiveCommands();
+
+        assertThat(checker.isStopped()).isTrue();
+        assertThat(checker.canUse()).isFalse();
+    }
+
+    @Test
+    public void shouldHandleNonExistentProcess() {
+        // Use an invalid PID that's unlikely to exist
+        ProcessChecker checker = 
ProcessChecker.of(Long.toString(Long.MAX_VALUE));
+
+        assertThat(checker).isNotNull();
+
+        assertThat(checker.canUse()).isTrue();
+
+        assertThat(checker.isProcessAlive()).isFalse();
+    }
+
+    /**
+     * Gets the current process PID in a way that works on both Java 8 and 
Java 9+.
+     */
+    private static String getCurrentPid() {
+        // Try ProcessHandle (Java 9+) first via reflection
+        try {
+            Class<?> processHandleClass = 
Class.forName("java.lang.ProcessHandle");
+            Object currentHandle = 
processHandleClass.getMethod("current").invoke(null);
+            Long pid = (Long) 
processHandleClass.getMethod("pid").invoke(currentHandle);
+            return String.valueOf(pid);
+        } catch (Exception e) {
+            // Fall back to ManagementFactory (works on Java 8)
+            try {
+                String name = 
java.lang.management.ManagementFactory.getRuntimeMXBean()
+                        .getName();
+                // Format is "pid@hostname"
+                int atIndex = name.indexOf('@');
+                if (atIndex > 0) {
+                    return name.substring(0, atIndex);
+                }
+            } catch (Exception ex) {
+                // Ignore
+            }
+        }
+        return null;
+    }
+}
diff --git 
a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ProcessHandleCheckerTest.java
 
b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ProcessHandleCheckerTest.java
new file mode 100644
index 000000000..cc60e6122
--- /dev/null
+++ 
b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ProcessHandleCheckerTest.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.surefire.booter;
+
+import java.lang.management.ManagementFactory;
+
+import org.junit.Assume;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assume.assumeTrue;
+
+/**
+ * Tests for {@link ProcessHandleChecker}.
+ * <p>
+ * These tests use reflection-based PID detection to work on both Java 8 and 
Java 9+.
+ */
+public class ProcessHandleCheckerTest {
+
+    @Test
+    public void shouldReportAvailableOnJava9Plus() {
+        // This test runs on modern JVMs, so isAvailable() should return true
+        // FIXME DisabledOnJre when we migrate to junit5
+        double v = 
Double.parseDouble(System.getProperty("java.specification.version"));
+        Assume.assumeTrue(v >= 9.0);
+        assertThat(ProcessHandleChecker.isAvailable()).isTrue();
+    }
+
+    @Test
+    public void shouldDetectCurrentProcessAsAlive() {
+        assumeTrue("ProcessHandle not available", 
ProcessHandleChecker.isAvailable());
+
+        String currentPid = getCurrentPid();
+        assumeTrue("Could not determine current PID", currentPid != null);
+
+        ProcessHandleChecker checker = new ProcessHandleChecker(currentPid);
+
+        assertThat(checker.canUse()).isTrue();
+        assertThat(checker.isProcessAlive()).isTrue();
+        assertThat(checker.isStopped()).isFalse();
+    }
+
+    @Test
+    public void shouldDetectNonExistentProcessAsNotUsable() {
+        assumeTrue("ProcessHandle not available", 
ProcessHandleChecker.isAvailable());
+
+        // Use an invalid PID that's unlikely to exist
+        ProcessHandleChecker checker = new ProcessHandleChecker("999999999");
+
+        assertThat(checker.canUse()).isTrue();
+    }
+
+    @Test
+    public void shouldStopChecker() {
+        assumeTrue("ProcessHandle not available", 
ProcessHandleChecker.isAvailable());
+
+        String currentPid = getCurrentPid();
+        assumeTrue("Could not determine current PID", currentPid != null);
+
+        ProcessHandleChecker checker = new ProcessHandleChecker(currentPid);
+
+        assertThat(checker.canUse()).isTrue();
+        assertThat(checker.isStopped()).isFalse();
+
+        checker.stop();
+
+        assertThat(checker.isStopped()).isTrue();
+        assertThat(checker.canUse()).isFalse();
+    }
+
+    @Test
+    public void shouldDestroyActiveCommands() {
+        assumeTrue("ProcessHandle not available", 
ProcessHandleChecker.isAvailable());
+
+        String currentPid = getCurrentPid();
+        assumeTrue("Could not determine current PID", currentPid != null);
+
+        ProcessHandleChecker checker = new ProcessHandleChecker(currentPid);
+
+        assertThat(checker.canUse()).isTrue();
+
+        checker.destroyActiveCommands();
+
+        assertThat(checker.isStopped()).isTrue();
+        assertThat(checker.canUse()).isFalse();
+    }
+
+    @Test
+    public void shouldReturnMeaningfulToString() {
+        assumeTrue("ProcessHandle not available", 
ProcessHandleChecker.isAvailable());
+
+        String currentPid = getCurrentPid();
+        assumeTrue("Could not determine current PID", currentPid != null);
+
+        ProcessHandleChecker checker = new ProcessHandleChecker(currentPid);
+
+        String toString = checker.toString();
+
+        assertThat(toString)
+                .contains("ProcessHandleChecker")
+                .contains("pid=" + currentPid)
+                .contains("stopped=false");
+    }
+
+    @Test
+    public void shouldReturnToStringWithStartInstantAfterCanUse() {
+        assumeTrue("ProcessHandle not available", 
ProcessHandleChecker.isAvailable());
+
+        String currentPid = getCurrentPid();
+        assumeTrue("Could not determine current PID", currentPid != null);
+
+        ProcessHandleChecker checker = new ProcessHandleChecker(currentPid);
+
+        checker.canUse();
+        String toString = checker.toString();
+
+        
assertThat(toString).contains("ProcessHandleChecker").contains("hasHandle=true");
+    }
+
+    @Test
+    public void shouldCreateViaFactoryMethod() {
+        assumeTrue("ProcessHandle not available", 
ProcessHandleChecker.isAvailable());
+
+        String currentPid = getCurrentPid();
+        assumeTrue("Could not determine current PID", currentPid != null);
+
+        ProcessChecker checker = ProcessChecker.of(currentPid);
+
+        assertThat(checker).isInstanceOf(ProcessHandleChecker.class);
+        assertThat(checker.canUse()).isTrue();
+        assertThat(checker.isProcessAlive()).isTrue();
+    }
+
+    @Test
+    public void shouldReturnNullFromFactoryForNullPpid() {
+        ProcessChecker checker = ProcessChecker.of(null);
+
+        assertThat(checker).isNull();
+    }
+
+    @Test
+    public void shouldThrowOnInvalidPpidFormat() {
+        assertThatThrownBy(() -> new 
ProcessHandleChecker("not-a-number")).isInstanceOf(NumberFormatException.class);
+    }
+
+    @Test
+    public void shouldReturnProcessInfoAfterCanUse() {
+        assumeTrue("ProcessHandle not available", 
ProcessHandleChecker.isAvailable());
+
+        String currentPid = getCurrentPid();
+        assumeTrue("Could not determine current PID", currentPid != null);
+
+        ProcessHandleChecker checker = new ProcessHandleChecker(currentPid);
+
+        // Now processInfo() should return valid info
+        ProcessInfo processInfo = checker.processInfo();
+        assertThat(processInfo).isNotNull();
+        assertThat(processInfo.getPID()).isEqualTo(currentPid);
+        assertThat(processInfo.getTime()).isGreaterThan(0L);
+    }
+
+    /**
+     * Gets the current process PID using reflection (Java 8 compatible).
+     *
+     * @return the current process PID as a string, or null if it cannot be 
determined
+     */
+    private static String getCurrentPid() {
+        // Try ProcessHandle.current().pid() via reflection (Java 9+)
+        try {
+            Class<?> processHandleClass = 
Class.forName("java.lang.ProcessHandle");
+            Object currentHandle = 
processHandleClass.getMethod("current").invoke(null);
+            Long pid = (Long) 
processHandleClass.getMethod("pid").invoke(currentHandle);
+            return String.valueOf(pid);
+        } catch (Exception e) {
+            // Fall back to ManagementFactory (works on Java 8)
+            try {
+                String name = ManagementFactory.getRuntimeMXBean().getName();
+                int atIndex = name.indexOf('@');
+                if (atIndex > 0) {
+                    return name.substring(0, atIndex);
+                }
+            } catch (Exception ex) {
+                // Ignore
+            }
+        }
+        return null;
+    }
+}

Reply via email to