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;
+ }
+}