Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
760 lines
31 KiB
Java
760 lines
31 KiB
Java
/*
|
|
* Copyright (c) 2025, 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.
|
|
*/
|
|
package jdk.java.lang.Process;
|
|
|
|
import org.junit.jupiter.api.Assertions;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.params.ParameterizedTest;
|
|
import org.junit.jupiter.params.provider.Arguments;
|
|
import org.junit.jupiter.params.provider.MethodSource;
|
|
|
|
import java.io.FilterInputStream;
|
|
import java.io.FilterOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.io.PrintStream;
|
|
import java.io.Reader;
|
|
import java.io.UncheckedIOException;
|
|
import java.io.Writer;
|
|
import java.lang.reflect.Field;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.time.Duration;
|
|
import java.time.Instant;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.concurrent.ForkJoinPool;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.function.Consumer;
|
|
import java.util.stream.Stream;
|
|
|
|
/*
|
|
* @test
|
|
* @bug 8364361
|
|
* @summary Tests for Process.close
|
|
* @modules java.base/java.lang:+open java.base/java.io:+open
|
|
* @run junit jdk.java.lang.Process.ProcessCloseTest
|
|
*/
|
|
public class ProcessCloseTest {
|
|
|
|
private final static boolean OS_WINDOWS = System.getProperty("os.name").startsWith("Windows");
|
|
private final static String CAT_PROGRAM = "cat";
|
|
public static final String FORCED_CLOSE_MSG = "Forced close";
|
|
private static List<String> JAVA_ARGS;
|
|
|
|
private static List<String> setupJavaEXE() {
|
|
String JAVA_HOME = System.getProperty("test.jdk");
|
|
if (JAVA_HOME == null)
|
|
JAVA_HOME = System.getProperty("JAVA_HOME");
|
|
String classPath = System.getProperty("test.class.path");
|
|
return List.of(JAVA_HOME + "/bin/java", "-cp", classPath, ProcessCloseTest.class.getName());
|
|
}
|
|
|
|
private static List<String> javaArgs(ChildCommand... moreArgs) {
|
|
|
|
List<String> javaArgs = JAVA_ARGS;
|
|
if (javaArgs == null) {
|
|
JAVA_ARGS = javaArgs = setupJavaEXE();
|
|
}
|
|
List<String> args = new ArrayList<>(javaArgs);
|
|
for (ChildCommand arg : moreArgs) {
|
|
args.add(arg.toString());
|
|
}
|
|
return args;
|
|
}
|
|
|
|
/**
|
|
* {@return A Stream of Arguments}
|
|
* Each Argument consists of three lists.
|
|
* - A List of command line arguments to start a process.
|
|
* `javaArgs can be used to launch a Java child with ChildCommands
|
|
* - A List of ProcessCommand actions to be invoked on that process
|
|
* - A List of commands to be invoked on the process after the close or T-W-R exit.
|
|
*/
|
|
static Stream<Arguments> singleThreadTestCases() {
|
|
return Stream.of(
|
|
Arguments.of(List.of("echo", "xyz0"),
|
|
List.of(ProcessCommand.STDOUT_PRINT_ALL_LINES,
|
|
ProcessCommand.STDERR_EXPECT_EMPTY,
|
|
ExitStatus.NORMAL),
|
|
List.of(ExitStatus.NORMAL)),
|
|
Arguments.of(List.of("echo", "xyz1"),
|
|
List.of(ProcessCommand.STDOUT_PRINT_ALL_LINES),
|
|
List.of(ExitStatus.NORMAL)),
|
|
Arguments.of(javaArgs(ChildCommand.STDOUT_ECHO),
|
|
List.of(ProcessCommand.WRITER_WRITE,
|
|
ProcessCommand.WRITER_CLOSE,
|
|
ProcessCommand.STDOUT_PRINT_ALL_LINES),
|
|
List.of(ExitStatus.NORMAL)),
|
|
Arguments.of(javaArgs(ChildCommand.STDOUT_ECHO),
|
|
List.of(ProcessCommand.STDOUT_WRITE,
|
|
ProcessCommand.STDOUT_CLOSE,
|
|
ProcessCommand.STDOUT_PRINT_ALL_LINES),
|
|
List.of(ExitStatus.NORMAL)),
|
|
Arguments.of(javaArgs(ChildCommand.STDOUT_ECHO),
|
|
List.of(ProcessCommand.STDOUT_WRITE,
|
|
ProcessCommand.STDOUT_CLOSE,
|
|
ExitStatus.NORMAL),
|
|
List.of(ExitStatus.NORMAL)),
|
|
Arguments.of(javaArgs(ChildCommand.STDERR_MARCO, ChildCommand.PROCESS_EXIT1),
|
|
List.of(ProcessCommand.STDERR_EXPECT_POLO,
|
|
ProcessCommand.STDOUT_EXPECT_EMPTY),
|
|
List.of(ExitStatus.FAIL)),
|
|
Arguments.of(javaArgs(ChildCommand.STDOUT_MARCO),
|
|
List.of(ProcessCommand.STDOUT_EXPECT_POLO,
|
|
ProcessCommand.STDERR_EXPECT_EMPTY),
|
|
List.of(ExitStatus.NORMAL)),
|
|
Arguments.of(javaArgs(ChildCommand.STDERR_MARCO),
|
|
List.of(ProcessCommand.STDERR_EXPECT_POLO,
|
|
ProcessCommand.STDOUT_EXPECT_EMPTY),
|
|
List.of(ExitStatus.NORMAL)),
|
|
Arguments.of(javaArgs(ChildCommand.PROCESS_EXIT1),
|
|
List.of(ExitStatus.FAIL),
|
|
List.of(ExitStatus.FAIL)), // Got expected status == 1
|
|
Arguments.of(javaArgs(ChildCommand.SLEEP),
|
|
List.of(ProcessCommand.PROCESS_INTERRUPT, // schedule an interrupt (in .2 sec)
|
|
ProcessCommand.PROCESS_CLOSE,
|
|
ProcessCommand.PROCESS_CHECK_INTERRUPTED), // Verify interrupted status
|
|
List.of(ExitStatus.KILLED)), // And process was destroyed
|
|
Arguments.of(javaArgs(ChildCommand.SLEEP),
|
|
List.of(ProcessCommand.PROCESS_INTERRUPT), // interrupts the TWR/close
|
|
List.of(ProcessCommand.PROCESS_CHECK_INTERRUPTED, ExitStatus.KILLED)),
|
|
Arguments.of(javaArgs(ChildCommand.SLEEP),
|
|
List.of(ExitStatus.NORMAL), // waitFor before T-W-R exit
|
|
List.of(ExitStatus.NORMAL)),
|
|
Arguments.of(List.of("echo", "abc"),
|
|
List.of(ProcessCommand.PROCESS_CLOSE),
|
|
List.of(ExitStatus.RACY)),
|
|
Arguments.of(javaArgs(ChildCommand.STDOUT_ECHO),
|
|
List.of(ProcessCommand.STDOUT_WRITE,
|
|
ProcessCommand.PROCESS_CLOSE),
|
|
List.of(ExitStatus.PIPE)),
|
|
Arguments.of(List.of("echo", "def"),
|
|
List.of(ProcessCommand.PROCESS_DESTROY),
|
|
List.of(ExitStatus.RACY)), // Racy, not deterministic
|
|
Arguments.of(javaArgs(ChildCommand.STDOUT_ECHO),
|
|
List.of(ProcessCommand.STDOUT_WRITE,
|
|
ProcessCommand.PROCESS_DESTROY),
|
|
List.of(ExitStatus.RACY)), // Racy, not deterministic
|
|
Arguments.of(List.of("echo"),
|
|
List.of(ExitStatus.NORMAL),
|
|
List.of(ExitStatus.NORMAL))
|
|
);
|
|
}
|
|
|
|
// Utility to process each command on the process
|
|
private static void doCommands(Process proc, List<Consumer<Process>> commands) {
|
|
commands.forEach(c -> {
|
|
Log.printf(" %s\n", c);
|
|
c.accept(proc);
|
|
});
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@MethodSource("singleThreadTestCases")
|
|
void simple(List<String> args, List<Consumer<Process>> commands,
|
|
List<Consumer<Process>> postCommands) throws IOException {
|
|
var log = Log.get();
|
|
try {
|
|
ProcessBuilder pb = new ProcessBuilder(args);
|
|
Process p = pb.start(); // Buffer any debug output
|
|
Log.printf("Program: %s; pid: %d\n", args, p.pid());
|
|
doCommands(p, commands);
|
|
p.close();
|
|
doCommands(p, postCommands);
|
|
} catch (Throwable ex) {
|
|
System.err.print(log);
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@MethodSource("singleThreadTestCases")
|
|
void autoCloseable(List<String> args, List<Consumer<Process>> commands,
|
|
List<Consumer<Process>> postCommands) {
|
|
var log = Log.get();
|
|
try {
|
|
ProcessBuilder pb = new ProcessBuilder(args);
|
|
Process proc = null;
|
|
try (Process p = pb.start()) {
|
|
proc = p;
|
|
Log.printf("Program: %s; pid: %d\n", args, p.pid());
|
|
doCommands(p, commands);
|
|
} catch (IOException ioe) {
|
|
Assertions.fail(ioe);
|
|
}
|
|
doCommands(proc, postCommands);
|
|
} catch (Throwable ex) {
|
|
System.err.println(log);
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test AutoCloseable for the process and out, in, and err streams.
|
|
* @param args The command line arguments
|
|
* @param commands the commands to the process
|
|
* @param postCommands The expected final exit status
|
|
*/
|
|
@ParameterizedTest
|
|
@MethodSource("singleThreadTestCases")
|
|
void autoCloseableAll(List<String> args, List<Consumer<Process>> commands,
|
|
List<Consumer<Process>> postCommands) {
|
|
var log = Log.get();
|
|
try {
|
|
ProcessBuilder pb = new ProcessBuilder(args);
|
|
Process proc = null;
|
|
try (Process p = pb.start(); var out = p.getOutputStream();
|
|
var in = p.getInputStream();
|
|
var err = p.getErrorStream()) {
|
|
proc = p;
|
|
Log.printf("Program: %s; pid: %d\n", args, p.pid());
|
|
doCommands(p, commands);
|
|
} catch (IOException ioe) {
|
|
Assertions.fail(ioe);
|
|
}
|
|
doCommands(proc, postCommands);
|
|
} catch (Throwable ex) {
|
|
System.err.println(log);
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ExitStatus named values and assertions
|
|
*/
|
|
enum ExitStatus implements Consumer<Process> {
|
|
NORMAL(0),
|
|
FAIL(1),
|
|
PIPE(0, 1, 141), // SIGPIPE
|
|
KILLED(1, 137), // SIGKILL
|
|
TERMINATED(0, 143), // SIGTERM
|
|
RACY(0, 1, 137, 143),
|
|
;
|
|
private final int[] allowedStatus;
|
|
|
|
ExitStatus(int... status) {
|
|
this.allowedStatus = status;
|
|
}
|
|
|
|
// If used as a process command, checks the exit status
|
|
public void accept(Process p) {
|
|
try {
|
|
Instant begin = Instant.now();
|
|
final int exitStatus = p.waitFor();
|
|
Duration latency = begin.until(Instant.now());
|
|
Log.printf(" ExitStatus: %d, sig#: %d, waitFor latency: %s%n",
|
|
exitStatus, exitStatus & 0x7f, latency);
|
|
assertEquals(exitStatus);
|
|
} catch (InterruptedException ie) {
|
|
Assertions.fail("Unexpected InterruptedException checking status: " + this);
|
|
}
|
|
}
|
|
|
|
// Check a status matches one of the allowed exit status values
|
|
void assertEquals(int actual) {
|
|
for (int status : allowedStatus) {
|
|
if (status == actual) {
|
|
return; // status is expected
|
|
}
|
|
}
|
|
if (this == RACY) {
|
|
// Not an error but report the actual status
|
|
Log.printf("Racy exit status: %d\n", actual);
|
|
} else {
|
|
Assertions.fail("Status: " + actual + ", sig#: " + (actual & 0x7f) +
|
|
", expected one of: " + Arrays.toString(allowedStatus));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Commands on a Process that can be sequenced in the parent.
|
|
* See ChildCommands for commands that can be sent to the child process.
|
|
*/
|
|
enum ProcessCommand implements Consumer<Process> {
|
|
PROCESS_CLOSE(ProcessCommand::processClose),
|
|
PROCESS_DESTROY(ProcessCommand::processDestroy),
|
|
PROCESS_FORCE_OUT_CLOSE_EXCEPTION(ProcessCommand::processForceOutCloseException),
|
|
PROCESS_FORCE_IN_CLOSE_EXCEPTION(ProcessCommand::processForceInCloseException),
|
|
PROCESS_FORCE_ERROR_CLOSE_EXCEPTION(ProcessCommand::processForceErrorCloseException),
|
|
WRITER_WRITE(ProcessCommand::writerWrite),
|
|
WRITER_CLOSE(ProcessCommand::writerClose),
|
|
STDOUT_PRINT_ALL_LINES(ProcessCommand::stdoutPrintAllLines),
|
|
STDERR_PRINT_ALL_LINES(ProcessCommand::stderrPrintAllLines),
|
|
STDOUT_WRITE(ProcessCommand::stdoutWrite),
|
|
STDOUT_CLOSE(ProcessCommand::stdoutClose),
|
|
STDOUT_EXPECT_POLO(ProcessCommand::stdoutExpectPolo),
|
|
STDERR_EXPECT_POLO(ProcessCommand::stderrExpectPolo),
|
|
STDOUT_EXPECT_EMPTY(ProcessCommand::stdoutExpectEmpty),
|
|
STDERR_EXPECT_EMPTY(ProcessCommand::stderrExpectEmpty),
|
|
PROCESS_INTERRUPT(ProcessCommand::processInterruptThread),
|
|
PROCESS_CHECK_INTERRUPTED(ProcessCommand::processAssertInterrupted),
|
|
;
|
|
private final Consumer<Process> command;
|
|
|
|
ProcessCommand(Consumer<Process> command) {
|
|
this.command = command;
|
|
}
|
|
|
|
public void accept(Process p) {
|
|
command.accept(p);
|
|
}
|
|
|
|
private static void stdoutPrintAllLines(Process p) {
|
|
try {
|
|
var lines = p.inputReader().readAllLines();
|
|
Assertions.assertNotEquals(0, lines.size(), "stdout should not be empty");
|
|
Log.printf(" %d lines\n", lines.size());
|
|
Log.printf("%s%n", lines.toString().indent(8));
|
|
} catch (IOException ioe) {
|
|
throw new UncheckedIOException(ioe);
|
|
}
|
|
}
|
|
|
|
private static void stderrPrintAllLines(Process p) {
|
|
try {
|
|
var lines = p.errorReader().readAllLines();
|
|
Assertions.assertNotEquals(0, lines.size(), "stderr should not be empty");
|
|
Log.printf(" %d lines\n", lines.size());
|
|
Log.printf("%s%n", lines.toString().indent(8));
|
|
} catch (IOException ioe) {
|
|
throw new UncheckedIOException(ioe);
|
|
}
|
|
}
|
|
|
|
private static void writerWrite(Process p) {
|
|
try {
|
|
p.outputWriter().write("Now is the time.");
|
|
} catch (IOException ioe) {
|
|
throw new UncheckedIOException(ioe);
|
|
}
|
|
}
|
|
|
|
private static void writerClose(Process p) {
|
|
try {
|
|
p.outputWriter().close();
|
|
} catch (IOException ioe) {
|
|
throw new UncheckedIOException(ioe);
|
|
}
|
|
}
|
|
|
|
private static void stdoutExpectPolo(Process p) {
|
|
String line = readLine(p.getInputStream());
|
|
Assertions.assertEquals("Polo", line, "Stdout Expected Polo");
|
|
}
|
|
|
|
private static void stderrExpectPolo(Process p) {
|
|
String line = readLine(p.getErrorStream());
|
|
Assertions.assertEquals("Polo", line, "Stderr Expected Polo");
|
|
}
|
|
|
|
private static void stdoutExpectEmpty(Process p) {
|
|
String line = readLine(p.getInputStream());
|
|
Assertions.assertEquals("", line, "Stdout Expected Empty");
|
|
}
|
|
|
|
private static void stderrExpectEmpty(Process p) {
|
|
String line = readLine(p.getErrorStream());
|
|
Assertions.assertEquals("", line, "Stderr Expected Empty");
|
|
}
|
|
|
|
private static String readLine(InputStream in) {
|
|
StringBuilder sb = new StringBuilder();
|
|
try {
|
|
int ch;
|
|
while ((ch = in.read()) != -1) {
|
|
if (ch == '\n') {
|
|
// end of line
|
|
return sb.toString();
|
|
}
|
|
if (ch != '\r') { // ignore cr - Windows defense
|
|
sb.append((char) ch);
|
|
}
|
|
}
|
|
// EOF - return string if no LF found
|
|
return sb.toString();
|
|
} catch (IOException ioe) {
|
|
return ioe.getMessage();
|
|
}
|
|
}
|
|
|
|
private static void stdoutWrite(Process p) {
|
|
try {
|
|
var out = p.getOutputStream();
|
|
out.write("stdout-write".getBytes(StandardCharsets.UTF_8));
|
|
out.flush();
|
|
} catch (IOException ioe) {
|
|
throw new UncheckedIOException(ioe);
|
|
}
|
|
}
|
|
|
|
private static void stdoutClose(Process p) {
|
|
try {
|
|
p.getOutputStream().close();
|
|
} catch (IOException ioe) {
|
|
throw new UncheckedIOException(ioe);
|
|
}
|
|
}
|
|
|
|
private static void processClose(Process p) {
|
|
try {
|
|
p.close();
|
|
} catch (IOException ioe) {
|
|
Assertions.fail(ioe);
|
|
}
|
|
}
|
|
|
|
private static void processDestroy(Process p) {
|
|
p.destroy();
|
|
}
|
|
|
|
// Interpose an input stream that throws on close()
|
|
private static void processForceInCloseException(Process p) {
|
|
try {
|
|
synchronized (p) {
|
|
Field stdinField = p.getClass().getDeclaredField(OS_WINDOWS ? "stdin_stream" : "stdin");
|
|
stdinField.setAccessible(true);
|
|
stdinField.set(p, new ThrowingOutputStream((OutputStream) stdinField.get(p)));
|
|
}
|
|
} catch (Exception ex) {
|
|
Assertions.fail("Failed to setup InputStream for throwing close", ex);
|
|
}
|
|
}
|
|
|
|
// Interpose an output stream that throws on close()
|
|
private static void processForceOutCloseException(Process p) {
|
|
try {
|
|
synchronized (p) {
|
|
Field stdoutField = p.getClass().getDeclaredField(OS_WINDOWS ? "stdout_stream" : "stdout");
|
|
stdoutField.setAccessible(true);
|
|
stdoutField.set(p, new ThrowingInputStream((InputStream) stdoutField.get(p)));
|
|
}
|
|
} catch (Exception ex) {
|
|
Assertions.fail("Failed to setup OutputStream throwing close", ex);
|
|
}
|
|
}
|
|
|
|
// Interpose an error stream that throws on close()
|
|
private static void processForceErrorCloseException(Process p) {
|
|
try {
|
|
synchronized (p) {
|
|
Field stderrField = p.getClass().getDeclaredField(OS_WINDOWS ? "stderr_stream" : "stderr");
|
|
stderrField.setAccessible(true);
|
|
stderrField.set(p, new ThrowingInputStream((InputStream) stderrField.get(p)));
|
|
}
|
|
} catch (Exception ex) {
|
|
Assertions.fail("Failed to setup OutputStream for throwing close", ex);
|
|
}
|
|
}
|
|
|
|
// Hard coded to interrupt the invoking thread at a fixed rate of .2 second, if process is alive
|
|
private static void processInterruptThread(Process p) {
|
|
if (p.isAlive()) {
|
|
int delay = 200;
|
|
final Thread targetThread = Thread.currentThread();
|
|
ForkJoinPool common = ForkJoinPool.commonPool();
|
|
final ThreadInterruptor interrupter = new ThreadInterruptor(p, targetThread);
|
|
common.scheduleAtFixedRate(interrupter, delay, delay, TimeUnit.MILLISECONDS);
|
|
}
|
|
}
|
|
|
|
// Verify that an interrupt is pending and reset it
|
|
private static void processAssertInterrupted(Process p) {
|
|
Assertions.assertTrue(Thread.interrupted(), "Expected an interrupt");
|
|
}
|
|
}
|
|
|
|
// Runnable scheduled at a fixed rate to interrupt a thread if a process is alive.
|
|
private static class ThreadInterruptor implements Runnable {
|
|
private final Process process;
|
|
private final Thread targetThread;
|
|
private int count;
|
|
|
|
ThreadInterruptor(Process process, Thread targetThread) {
|
|
this.process = process;
|
|
this.targetThread = targetThread;
|
|
this.count = 0;
|
|
}
|
|
|
|
public void run() {
|
|
if (process.isAlive()) {
|
|
count++;
|
|
Log.printf("Interrupting thread, count: %d%n", count);
|
|
targetThread.interrupt();
|
|
} else {
|
|
throw new RuntimeException("process not alive");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Commands to Java child sent as command line arguments
|
|
enum ChildCommand {
|
|
STDOUT_ECHO(ChildCommand::stdoutEchoBytes),
|
|
STDERR_ECHO(ChildCommand::stderrEchoBytes),
|
|
SLEEP(ChildCommand::SLEEP),
|
|
STDOUT_MARCO(ChildCommand::stdoutMarco),
|
|
STDERR_MARCO(ChildCommand::stderrMarco),
|
|
PROCESS_EXIT1(ChildCommand::processExit1),
|
|
;
|
|
private final Runnable command;
|
|
ChildCommand(Runnable cmd) {
|
|
this.command = cmd;
|
|
}
|
|
|
|
// The child sleeps before continuing with next ChildCommand
|
|
private static void SLEEP() {
|
|
final int sleepMS = 2_000;
|
|
try {
|
|
Thread.sleep(sleepMS);
|
|
} catch (InterruptedException ie) {
|
|
// Interrupted sleep, re-assert interrupted status
|
|
System.err.println("Sleep interrupted"); // Note the interruption in the log
|
|
Thread.currentThread().interrupt();
|
|
}
|
|
}
|
|
|
|
private static void stdoutEchoBytes() {
|
|
echoBytes(System.in, System.out);
|
|
}
|
|
|
|
private static void stderrEchoBytes() {
|
|
echoBytes(System.in, System.err);
|
|
}
|
|
|
|
private static void echoBytes(InputStream in, PrintStream out) {
|
|
try {
|
|
byte[] bytes = in.readAllBytes();
|
|
out.write(bytes);
|
|
} catch (IOException ioe) {
|
|
out.println(ioe);
|
|
}
|
|
}
|
|
|
|
private static void stdoutMarco() {
|
|
System.out.println("Polo");
|
|
}
|
|
|
|
private static void stderrMarco() {
|
|
System.err.println("Polo");
|
|
}
|
|
|
|
private static void processExit1() {
|
|
System.exit(1);
|
|
}
|
|
}
|
|
|
|
static Stream<Arguments> closeExceptions() {
|
|
return Stream.of(
|
|
Arguments.of(javaArgs(ChildCommand.SLEEP),
|
|
List.of(ProcessCommand.PROCESS_FORCE_OUT_CLOSE_EXCEPTION),
|
|
List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG)),
|
|
Arguments.of(javaArgs(ChildCommand.SLEEP),
|
|
List.of(ProcessCommand.PROCESS_FORCE_IN_CLOSE_EXCEPTION),
|
|
List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG)),
|
|
Arguments.of(javaArgs(ChildCommand.SLEEP),
|
|
List.of(ProcessCommand.PROCESS_FORCE_ERROR_CLOSE_EXCEPTION),
|
|
List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG)),
|
|
Arguments.of(javaArgs(ChildCommand.SLEEP),
|
|
List.of(ProcessCommand.PROCESS_FORCE_OUT_CLOSE_EXCEPTION,
|
|
ProcessCommand.PROCESS_FORCE_IN_CLOSE_EXCEPTION,
|
|
ProcessCommand.PROCESS_FORCE_ERROR_CLOSE_EXCEPTION),
|
|
List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG, FORCED_CLOSE_MSG, FORCED_CLOSE_MSG)),
|
|
Arguments.of(javaArgs(ChildCommand.SLEEP),
|
|
List.of(ProcessCommand.PROCESS_FORCE_OUT_CLOSE_EXCEPTION,
|
|
ProcessCommand.PROCESS_FORCE_IN_CLOSE_EXCEPTION),
|
|
List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG, FORCED_CLOSE_MSG)),
|
|
Arguments.of(javaArgs(ChildCommand.SLEEP),
|
|
List.of(ProcessCommand.PROCESS_FORCE_OUT_CLOSE_EXCEPTION,
|
|
ProcessCommand.PROCESS_FORCE_ERROR_CLOSE_EXCEPTION),
|
|
List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG, FORCED_CLOSE_MSG)),
|
|
Arguments.of(javaArgs(ChildCommand.SLEEP),
|
|
List.of(ProcessCommand.PROCESS_FORCE_IN_CLOSE_EXCEPTION,
|
|
ProcessCommand.PROCESS_FORCE_ERROR_CLOSE_EXCEPTION),
|
|
List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG, FORCED_CLOSE_MSG)),
|
|
Arguments.of(List.of("echo", "xyz1"),
|
|
List.of(ProcessCommand.PROCESS_FORCE_OUT_CLOSE_EXCEPTION),
|
|
List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG))
|
|
);
|
|
}
|
|
/**
|
|
* Test AutoCloseable for cases that are expected to throw exceptions.
|
|
* The list of ProcessCommands controls what is sent to the process and closing of streams.
|
|
* The command line arguments control the sequence of actions taken by the child.
|
|
* @param args The command line arguments for the child process
|
|
* @param commands the commands to this process controlling the child
|
|
* @param postCommands The expected final exit status
|
|
* @param expectedMessages the list of exception messages expected by close()
|
|
*/
|
|
@ParameterizedTest
|
|
@MethodSource("closeExceptions")
|
|
void testStreamsCloseThrowing(List<String> args, List<Consumer<Process>> commands,
|
|
List<Consumer<Process>> postCommands, List<String> expectedMessages) {
|
|
var log = Log.get();
|
|
try {
|
|
ProcessBuilder pb = new ProcessBuilder(args);
|
|
Process proc = null;
|
|
IOException expectedIOE = null;
|
|
try (Process p = pb.start()) {
|
|
proc = p;
|
|
Log.printf("Program: %s; pid: %d\n",args, p.pid());
|
|
doCommands(p, commands);
|
|
} catch (IOException ioe) {
|
|
expectedIOE = ioe;
|
|
}
|
|
// Check the exceptions thrown, if any
|
|
if (expectedIOE != null) {
|
|
// Check each exception that it is expected
|
|
Assertions.assertEquals(expectedMessages.getFirst(), expectedIOE.getMessage(),
|
|
"Unexpected exception message");
|
|
var suppressedEx = expectedIOE.getSuppressed();
|
|
Assertions.assertEquals(expectedMessages.size() - 1, suppressedEx.length,
|
|
"Number of suppressed exceptions");
|
|
for (int i = 1; i < expectedMessages.size(); i++) {
|
|
Assertions.assertEquals(expectedMessages.get(i),
|
|
suppressedEx[i - 1].getMessage(),
|
|
"Unexpected suppressed exception message");
|
|
}
|
|
}
|
|
Assertions.assertNotNull(proc, "Process is null");
|
|
doCommands(proc, postCommands);
|
|
} catch (Exception ex) {
|
|
System.err.println(log);
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An OutputStream that delegates to another stream and always throws IOException on close().
|
|
*/
|
|
private static class ThrowingOutputStream extends FilterOutputStream {
|
|
public ThrowingOutputStream(OutputStream out) {
|
|
super(out);
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
try {
|
|
out.close();
|
|
} catch (IOException ioe) {
|
|
// ignore except to log the exception; may be useful to debug
|
|
ioe.printStackTrace(System.err);
|
|
}
|
|
throw new IOException(FORCED_CLOSE_MSG);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An InputStream that delegates to another stream and always throws IOException on close().
|
|
*/
|
|
private static class ThrowingInputStream extends FilterInputStream {
|
|
public ThrowingInputStream(InputStream in) {
|
|
super(in);
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
try {
|
|
in.close();
|
|
} catch (IOException ioe) {
|
|
// ignore except to log the exception; may be useful to debug
|
|
ioe.printStackTrace(System.err);
|
|
}
|
|
throw new IOException(FORCED_CLOSE_MSG);
|
|
}
|
|
}
|
|
|
|
// Copy of ProcessExamples in java/lang/snippet-files/ProcessExamples.java
|
|
@Test
|
|
void example() {
|
|
try (Process p = new ProcessBuilder(CAT_PROGRAM).start();
|
|
Writer writer = p.outputWriter();
|
|
Reader reader = p.inputReader()) {
|
|
writer.write(haiku);
|
|
writer.close();
|
|
// Read all lines and print each
|
|
reader.readAllLines()
|
|
.forEach(System.out::println);
|
|
var status = p.waitFor();
|
|
if (status != 0)
|
|
throw new RuntimeException("unexpected process status: " + status);
|
|
} catch (Exception e) {
|
|
System.err.println("Process failed: " + e);
|
|
}
|
|
}
|
|
|
|
String haiku = """
|
|
Oh, the sunrise glow;
|
|
Paddling with the river flow;
|
|
Chilling still, go slow.
|
|
""";
|
|
|
|
/**
|
|
* Child program that executes child actions as named by command line args.
|
|
* @param childCommands a sequence of ChildCommand names.
|
|
*/
|
|
public static void main(String... childCommands) {
|
|
List<String> args = List.of(childCommands);
|
|
List<ChildCommand> commands = args.stream().map(ChildCommand::valueOf).toList();
|
|
commands.forEach(c -> c.command.run());
|
|
}
|
|
|
|
/**
|
|
* Log of output produced on a thread during a test.
|
|
* Normally, the output is buffered and only output to stderr if the test fails.
|
|
* Set -DDEBUG=true to send all output to stderr as it occurs.
|
|
*/
|
|
private static class Log {
|
|
|
|
private static final boolean DEBUG = Boolean.getBoolean("DEBUG");
|
|
private final static ScopedValue<Appendable> OUT = ScopedValue.newInstance();
|
|
private final static ScopedValue.Carrier LOG = setupLog();
|
|
|
|
private static ScopedValue.Carrier setupLog() {
|
|
if (DEBUG) {
|
|
return ScopedValue.where(OUT, System.err);
|
|
} else {
|
|
return ScopedValue.where(OUT, new StringBuffer());
|
|
}
|
|
}
|
|
|
|
// Return the log for this thread and clear the buffer.
|
|
private static Appendable get() {
|
|
var log = LOG.get(OUT);
|
|
if (log instanceof StringBuffer sb)
|
|
sb.setLength(0);
|
|
return log;
|
|
}
|
|
|
|
// Printf to the log for this thread.
|
|
private static void printf(String format, Object... args) {
|
|
try {
|
|
var log = LOG.get(OUT);
|
|
log.append(format.formatted(args));
|
|
} catch (IOException ioe) {
|
|
throw new UncheckedIOException(ioe);
|
|
}
|
|
}
|
|
}
|
|
}
|