| /* | |
| * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| import java.io.IOException; | |
| import java.time.Duration; | |
| import java.time.Instant; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.concurrent.ArrayBlockingQueue; | |
| import java.util.concurrent.CompletableFuture; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.concurrent.TimeUnit; | |
| import jdk.test.lib.Platform; | |
| import jdk.test.lib.Utils; | |
| import org.testng.annotations.Test; | |
| import org.testng.Assert; | |
| import org.testng.TestNG; | |
| /* | |
| * @test | |
| * @bug 8333742 | |
| * @requires vm.flagless | |
| * @library /test/lib | |
| * @modules jdk.management | |
| * @build jdk.test.lib.Utils | |
| * @run testng OnExitTest | |
| * @summary Functions of Process.onExit and ProcessHandle.onExit | |
| * @author Roger Riggs | |
| */ | |
| public class OnExitTest extends ProcessUtil { | |
| @SuppressWarnings("raw_types") | |
| public static void main(String[] args) { | |
| Class<?>[] testclass = { OnExitTest.class}; | |
| TestNG testng = new TestNG(); | |
| testng.setTestClasses(testclass); | |
| testng.run(); | |
| } | |
| /** | |
| * Basic test of exitValue and onExit. | |
| */ | |
| @Test | |
| public static void test1() { | |
| try { | |
| int[] exitValues = {0, 1, 10, 259}; | |
| for (int value : exitValues) { | |
| // Linux & Mac don't support exit codes longer than 8 bits, skip | |
| if (value == 259 && !Platform.isWindows()) continue; | |
| Process p = JavaChild.spawn("exit", Integer.toString(value)); | |
| CompletableFuture<Process> future = p.onExit(); | |
| future.thenAccept( (ph) -> { | |
| int actualExitValue = ph.exitValue(); | |
| printf(" javaChild done: %s, exitStatus: %d%n", | |
| ph, actualExitValue); | |
| Assert.assertEquals(actualExitValue, value, "actualExitValue incorrect"); | |
| Assert.assertEquals(ph, p, "Different Process passed to thenAccept"); | |
| }); | |
| Process h = future.get(); | |
| Assert.assertEquals(h, p); | |
| Assert.assertEquals(p.exitValue(), value); | |
| Assert.assertFalse(p.isAlive(), "Process should not be alive"); | |
| Assert.assertEquals(p.waitFor(), value); | |
| Assert.assertTrue(p.waitFor(1, TimeUnit.MILLISECONDS), | |
| "waitFor should return true"); | |
| } | |
| } catch (IOException | InterruptedException | ExecutionException ex) { | |
| Assert.fail(ex.getMessage(), ex); | |
| } finally { | |
| destroyProcessTree(ProcessHandle.current()); | |
| } | |
| } | |
| /** | |
| * Test of Completion handler when parent is killed. | |
| * Spawn 1 child to spawn 3 children each with 2 children. | |
| */ | |
| @Test | |
| public static void test2() { | |
| // Please note (JDK-8284282): | |
| // | |
| // On Unix, this test relies on the ability of the system to adopt orphaned processes and | |
| // reap them in a timely fashion. In other words, the ability to prevent orphans from becoming | |
| // zombies. | |
| // | |
| // Therefore, on misconfigured or broken systems, this test may fail. These failures will manifest | |
| // as timeouts. The failures depend on timing: they may not happen at all, be intermittent or | |
| // constant. | |
| // | |
| // That will rarely be a problem on bare-metal systems but may be more common when running in | |
| // Docker. Misconfigured Docker instances may run with an initial process unable to reap. One | |
| // infamous example would be running jtreg tests inside a Docker via Jenkins CI. | |
| // | |
| // This is quite difficult - and inefficient - to fix inside this test, and rather easy to | |
| // avoid. For a detailed analysis, as well as proposed workarounds, please see JDK-8284282. | |
| // | |
| ProcessHandle procHandle = null; | |
| try { | |
| ConcurrentHashMap<ProcessHandle, ProcessHandle> processes = new ConcurrentHashMap<>(); | |
| List<ProcessHandle> children = getChildren(ProcessHandle.current()); | |
| children.forEach(ProcessUtil::printProcess); | |
| JavaChild proc = JavaChild.spawnJavaChild("stdin"); | |
| procHandle = proc.toHandle(); | |
| printf(" spawned: %d%n", proc.pid()); | |
| proc.forEachOutputLine((s) -> { | |
| String[] split = s.trim().split(" "); | |
| if (split.length == 3 && split[1].equals("spawn")) { | |
| Long child = Long.valueOf(split[2]); | |
| Long parent = Long.valueOf(split[0].split(":")[0]); | |
| processes.put(ProcessHandle.of(child).get(), ProcessHandle.of(parent).get()); | |
| } | |
| }); | |
| proc.sendAction("spawn", "3", "stdin"); | |
| proc.sendAction("child", "spawn", "2", "stdin"); | |
| // Poll until all 9 child processes exist or the timeout is reached | |
| int expected = 9; | |
| long timeout = Utils.adjustTimeout(10L); | |
| Instant endTimeout = Instant.now().plusSeconds(timeout); | |
| do { | |
| Thread.sleep(200L); | |
| printf(" subprocess count: %d, waiting for %d%n", processes.size(), expected); | |
| } while (processes.size() < expected && | |
| Instant.now().isBefore(endTimeout)); | |
| if (processes.size() < expected) { | |
| printf("WARNING: not all children have been started. Can't complete test.%n"); | |
| printf(" You can try to increase the timeout or%n"); | |
| printf(" you can try to use a faster VM (i.e. not a debug version).%n"); | |
| } | |
| children = getDescendants(procHandle); | |
| ConcurrentHashMap<ProcessHandle, CompletableFuture<ProcessHandle>> completions = | |
| new ConcurrentHashMap<>(); | |
| Instant startTime = Instant.now(); | |
| // Create a future for each of the 9 children | |
| processes.forEach( (p, parent) -> { | |
| CompletableFuture<ProcessHandle> cf = p.onExit().whenComplete((ph, ex) -> { | |
| Duration elapsed = Duration.between(startTime, Instant.now()); | |
| printf("whenComplete: pid: %s, exception: %s, thread: %s, elapsed: %s%n", | |
| ph, ex, Thread.currentThread(), elapsed); | |
| }); | |
| completions.put(p, cf); | |
| }); | |
| // Check that each of the spawned processes is included in the children | |
| List<ProcessHandle> remaining = new ArrayList<>(children); | |
| processes.forEach((p, parent) -> { | |
| Assert.assertTrue(remaining.remove(p), "spawned process should have been in children"); | |
| }); | |
| // Remove Win32 system spawned conhost.exe processes | |
| remaining.removeIf(ProcessUtil::isWindowsConsole); | |
| remaining.forEach(p -> printProcess(p, "unexpected: ")); | |
| if (remaining.size() > 0) { | |
| // Show full list for debugging | |
| ProcessUtil.logTaskList(); | |
| } | |
| proc.destroy(); // kill off the parent | |
| proc.waitFor(); | |
| // Wait for all the processes and corresponding onExit CF to be completed | |
| processes.forEach((p, parent) -> { | |
| try { | |
| p.onExit().get(); | |
| completions.get(p).join(); | |
| } catch (InterruptedException | ExecutionException ex) { | |
| // ignore | |
| } | |
| }); | |
| // Verify that all 9 exit handlers were called with the correct ProcessHandle | |
| processes.forEach((p, parent) -> { | |
| ProcessHandle value = completions.get(p).getNow(null); | |
| Assert.assertEquals(p, value, "onExit.get value expected: " + p | |
| + ", actual: " + value | |
| + ": " + p.info()); | |
| }); | |
| // Show the status of the original children | |
| children.forEach(p -> printProcess(p, "after onExit:")); | |
| Assert.assertEquals(proc.isAlive(), false, "destroyed process is alive:: %s%n" + proc); | |
| } catch (IOException | InterruptedException ex) { | |
| Assert.fail(ex.getMessage()); | |
| } finally { | |
| if (procHandle != null) { | |
| destroyProcessTree(procHandle); | |
| } | |
| } | |
| } | |
| /** | |
| * Verify that onExit completes for a non-child process only when | |
| * the process has exited. | |
| * Spawn a child (A) waiting to be commanded to exit. | |
| * Spawn a child (B) to wait for that process to exit. | |
| * Command (A) to exit. | |
| * Check that (B) does not complete until (A) has exited. | |
| */ | |
| @Test | |
| public static void peerOnExitTest() { | |
| String line = null; | |
| ArrayBlockingQueue<String> alines = new ArrayBlockingQueue<>(100); | |
| ArrayBlockingQueue<String> blines = new ArrayBlockingQueue<>(100); | |
| JavaChild A = null; | |
| try { | |
| String[] split; | |
| A = JavaChild.spawnJavaChild("stdin"); | |
| A.forEachOutputLine(l -> alines.add(l)); | |
| // Verify A is running | |
| A.sendAction("pid"); | |
| do { | |
| split = getSplitLine(alines); | |
| } while (!"pid".equals(split[1])); | |
| JavaChild B = null; | |
| try { | |
| B = JavaChild.spawnJavaChild("stdin"); | |
| B.forEachOutputLine(l -> blines.add(l)); | |
| // Verify B is running | |
| B.sendAction("pid"); | |
| do { | |
| split = getSplitLine(blines); | |
| } while (!"pid".equals(split[1])); | |
| // Tell B to wait for A's pid | |
| B.sendAction("waitpid", A.pid()); | |
| // Wait a bit to see if B will prematurely report the termination of A | |
| try { | |
| line = blines.poll(5L, TimeUnit.SECONDS); | |
| } catch (InterruptedException ie) { | |
| Assert.fail("interrupted", ie); | |
| } | |
| Assert.assertNull(line, "waitpid didn't wait"); | |
| A.toHandle().onExit().thenAccept(p -> { | |
| System.out.printf(" A.toHandle().onExit().A info: %s, now: %s%n", | |
| p.info(), Instant.now()); | |
| }); | |
| A.onExit().thenAccept(p -> { | |
| System.out.printf(" A.onExit().A info: %s, now: %s%n", | |
| p.info(), Instant.now()); | |
| }); | |
| ProcessHandle.Info A_info = A.info(); | |
| A.sendAction("exit", 0L); | |
| // Look for B to report that A has exited | |
| do { | |
| Instant start = Instant.now(); | |
| while (blines.isEmpty() && A.isAlive()) { | |
| A_info = A.info(); // Spin | |
| } | |
| Instant end = Instant.now(); | |
| System.out.printf(" a.isAlive: %s, a.info: %s, @%s%n", A.isAlive(), A.info(), | |
| Duration.between(start, end)); | |
| split = getSplitLine(blines); | |
| } while (!"waitpid".equals(split[1])); | |
| Assert.assertEquals(split[2], "false", "Process A should not be alive"); | |
| B.sendAction("exit", 0L); | |
| } catch (IOException ioe) { | |
| Assert.fail("unable to start JavaChild B", ioe); | |
| } finally { | |
| B.destroyForcibly(); | |
| } | |
| } catch (IOException ioe2) { | |
| Assert.fail("unable to start JavaChild A", ioe2); | |
| } finally { | |
| A.destroyForcibly(); | |
| } | |
| } | |
| private static boolean DEBUG = true; | |
| /** | |
| * Get a line from the queue and split into words on whitespace. | |
| * Log to stdout if requested. | |
| * @param queue a queue of strings | |
| * @return the words split from the line. | |
| */ | |
| private static String[] getSplitLine(ArrayBlockingQueue<String> queue) { | |
| try { | |
| String line = queue.take(); | |
| String[] split = line.split("\\s"); | |
| if (DEBUG) { | |
| System.out.printf(" Child Output: %s%n", line); | |
| } | |
| return split; | |
| } catch (InterruptedException ie) { | |
| Assert.fail("interrupted", ie); | |
| return null; | |
| } | |
| } | |
| } |