This is an automated email from the ASF dual-hosted git repository. tibordigana pushed a commit to branch revert-3252-ppidchecker-processhandle in repository https://gitbox.apache.org/repos/asf/maven-surefire.git
commit b829dd4cad2bdcbd079747c5113ed6576109d4a5 Author: Tibor Digana <[email protected]> AuthorDate: Sun Feb 15 00:19:10 2026 +0100 Revert "Replace runing external process and parsing output with simple Proces…" This reverts commit 0b190142a3df4cb3dda52825e7fedda59591cbc8. --- 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, 452 insertions(+), 851 deletions(-) diff --git a/maven-surefire-common/pom.xml b/maven-surefire-common/pom.xml index 072efcabf..a9feb6dd9 100644 --- a/maven-surefire-common/pom.xml +++ b/maven-surefire-common/pom.xml @@ -172,6 +172,7 @@ <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 e18803142..12d19c5e8 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,6 +80,11 @@ public Platform withJdkExecAttributesForTests(JdkAttributes jdk) { } private static Callable<Long> pidJob() { - return SystemUtils::pid; + return new Callable<Long>() { + @Override + public Long call() throws Exception { + 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 93083858d..92f9ddc6c 100644 --- a/maven-surefire-plugin/src/site/apt/examples/shutdown.apt.vm +++ b/maven-surefire-plugin/src/site/apt/examples/shutdown.apt.vm @@ -55,13 +55,9 @@ 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 <<< powershell -command "... Get-CimInstance Win32_Process ..." >>>. - - [] + On Windows the start time is determined using <<< wmic process where (ProcessId=[PID]) get CreationDate >>> in the forked JVM. diff --git a/surefire-booter/pom.xml b/surefire-booter/pom.xml index 74b055a9d..c24bb7e36 100644 --- a/surefire-booter/pom.xml +++ b/surefire-booter/pom.xml @@ -91,11 +91,6 @@ <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 5067509c3..1bcdc8b09 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) { - ProcessChecker ppidChecker = ProcessChecker.of(ppid); + PpidChecker ppidChecker = ppid == null ? null : new PpidChecker(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 ProcessChecker ppidChecker) { + private CommandListener createExitHandler(final PpidChecker 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 ProcessChecker pluginProcessChecker) { + private Runnable createPingJob(final AtomicBoolean pingDone, final PpidChecker pluginProcessChecker) { return new Runnable() { @Override public void run() { @@ -515,7 +515,7 @@ private static void run(ForkedBooter booter, String[] args) { } } - private static boolean canUseNewPingMechanism(ProcessChecker pluginProcessChecker) { + private static boolean canUseNewPingMechanism(PpidChecker 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 ProcessChecker processChecker; + private final PpidChecker processChecker; PingScheduler( ScheduledExecutorService pingScheduler, ScheduledExecutorService processCheckerScheduler, - ProcessChecker processChecker) { + PpidChecker 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 bfcc70d18..b8891e822 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,18 +57,11 @@ /** * 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 */ -@Deprecated -final class PpidChecker implements ProcessChecker { +final class PpidChecker { 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; @@ -102,8 +95,7 @@ final class PpidChecker implements ProcessChecker { this.ppid = ppid; } - @Override - public boolean canUse() { + boolean canUse() { if (isStopped()) { return false; } @@ -119,8 +111,7 @@ public boolean canUse() { * or this object has been {@link #destroyActiveCommands() destroyed} * @throws NullPointerException if extracted e-time is null */ - @Override - public boolean isProcessAlive() { + boolean isProcessAlive() { if (!canUse()) { throw new IllegalStateException("irrelevant to call isProcessAlive()"); } @@ -235,16 +226,14 @@ ProcessInfo consumeLine(String line, ProcessInfo previousProcessInfo) throws Exc return reader.execute(psPath + "powershell", "-NoProfile", "-NonInteractive", "-Command", psCommand); } - @Override - public void destroyActiveCommands() { + void destroyActiveCommands() { stopped = true; for (Process p = destroyableCommands.poll(); p != null; p = destroyableCommands.poll()) { p.destroy(); } } - @Override - public boolean isStopped() { + boolean isStopped() { return stopped; } @@ -336,16 +325,10 @@ 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 deleted file mode 100644 index ef495eb53..000000000 --- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessChecker.java +++ /dev/null @@ -1,108 +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; - -/** - * 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 deleted file mode 100644 index a97e6ace4..000000000 --- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ProcessHandleChecker.java +++ /dev/null @@ -1,238 +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.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 630c7dd5b..771445965 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,17 +48,6 @@ 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 35f5cf75e..3f7b4aa5b 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,7 +172,6 @@ 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 new file mode 100644 index 000000000..5b05d1a46 --- /dev/null +++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/PpidCheckerTest.java @@ -0,0 +1,432 @@ +/* + * 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 deleted file mode 100644 index ac5883d17..000000000 --- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ProcessCheckerTest.java +++ /dev/null @@ -1,248 +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 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 deleted file mode 100644 index cc60e6122..000000000 --- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ProcessHandleCheckerTest.java +++ /dev/null @@ -1,205 +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 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; - } -}
