diff --git a/src/java.base/share/classes/java/lang/Process.java b/src/java.base/share/classes/java/lang/Process.java index 3e4837d2e0253..0164675082dc1 100644 --- a/src/java.base/share/classes/java/lang/Process.java +++ b/src/java.base/share/classes/java/lang/Process.java @@ -78,9 +78,42 @@ * process I/O can also be redirected * using methods of the {@link ProcessBuilder} class. * - *

The process is not killed when there are no more references to - * the {@code Process} object, but rather the process - * continues executing asynchronously. + *

{@linkplain ProcessBuilder#start() Starting a process} uses resources in both the invoking process and the invoked + * process and for the communication streams between them. + * The resources to control the process and for communication between the processes are retained + * until there are no longer any references to the Process or the input, error, and output streams + * or readers, or they have been closed. + * + *

The process is not killed when there are no more references to the {@code Process} object, + * but rather the process continues executing asynchronously. + * The process implementation closes file descriptors and handles for streams + * that are no longer referenced to prevent leaking operating system resources. + * Processes that have terminated or been terminated are monitored and their resources released. + * + *

Streams should be {@code closed} when they are no longer needed, to avoid delaying + * releasing the operating system resources. + * {@code Try-with-resources} can be used to open and close the streams. + *

For example, to capture the output of a program known to produce some output and then exit: + * {@snippet lang = "java" : + * List capture(List args) throws Exception { + * ProcessBuilder pb = new ProcessBuilder().command(args); + * Process process = pb.start(); + * try (BufferedReader in = process.inputReader()) { + * List captured = in.lines().toList(); + * int status = process.waitFor(); + * if (status != 0) { + * throw new RuntimeException("Process %d: %s failed with %d" + * .formatted(process.pid(), args, status)); + * } + * return captured; + * } + * } + * } + *

Stream resources (file descriptor or handle) are always paired; one in the invoking process + * and the other end of that connection in the invoked process. + * Closing a stream at either end terminates communication but does not have any direct effect + * on the other Process. Typically, the other process responds to the closing of the stream + * by exiting. * *

There is no requirement that the process represented by a {@code * Process} object execute asynchronously or concurrently with respect @@ -127,6 +160,9 @@ public Process() {} * then this method will return a * null output stream. * + *

The output stream should be {@linkplain OutputStream#close closed} + * when it is no longer needed. + * * @apiNote * When writing to both {@link #getOutputStream()} and either {@link #outputWriter()} * or {@link #outputWriter(Charset)}, {@link BufferedWriter#flush BufferedWriter.flush} @@ -159,9 +195,14 @@ public Process() {} * then the input stream returned by this method will receive the * merged standard output and the standard error of the process. * + *

The input stream should be {@linkplain InputStream#close closed} + * when it is no longer needed. + * * @apiNote - * Use {@link #getInputStream()} and {@link #inputReader()} with extreme care. - * The {@code BufferedReader} may have buffered input from the input stream. + * Avoid using both {@link #getInputStream} and {@link #inputReader(Charset)}. + * The input reader consumes and buffers bytes from the input stream. + * Bytes read from the input stream would not be seen by the reader and + * buffer contents are unpredictable. * * @implNote * Implementation note: It is a good idea for the returned @@ -185,9 +226,14 @@ public Process() {} * then this method will return a * null input stream. * + *

The error stream should be {@linkplain InputStream#close closed} + * when it is no longer needed. + * * @apiNote - * Use {@link #getErrorStream()} and {@link #errorReader()} with extreme care. - * The {@code BufferedReader} may have buffered input from the error stream. + * Avoid using both {@link #getErrorStream} and {@link #inputReader(Charset)}. + * The error reader consumes and buffers bytes from the error stream. + * Bytes read from the error stream would not be seen by the reader and the + * buffer contents are unpredictable. * * @implNote * Implementation note: It is a good idea for the returned @@ -208,6 +254,9 @@ public Process() {} * If the {@code native.encoding} is not a valid charset name or not supported * the {@link Charset#defaultCharset()} is used. * + *

The reader should be {@linkplain BufferedReader#close closed} + * when it is no longer needed. + * * @return a {@link BufferedReader BufferedReader} using the * {@code native.encoding} if supported, otherwise, the * {@link Charset#defaultCharset()} @@ -238,6 +287,9 @@ public final BufferedReader inputReader() { * then the {@code InputStreamReader} will be reading from a * null input stream. * + *

The reader should be {@linkplain BufferedReader#close closed} + * when it is no longer needed. + * *

Otherwise, if the standard error of the process has been redirected using * {@link ProcessBuilder#redirectErrorStream(boolean) * ProcessBuilder.redirectErrorStream} then the input reader returned by @@ -245,9 +297,10 @@ public final BufferedReader inputReader() { * of the process. * * @apiNote - * Using both {@link #getInputStream} and {@link #inputReader(Charset)} has - * unpredictable behavior since the buffered reader reads ahead from the - * input stream. + * Avoid using both {@link #getInputStream} and {@link #inputReader(Charset)}. + * The input reader consumes and buffers bytes from the input stream. + * Bytes read from the input stream would not be seen by the reader and the + * buffer contents are unpredictable. * *

When the process has terminated, and the standard input has not been redirected, * reading of the bytes available from the underlying stream is on a best effort basis and @@ -283,6 +336,9 @@ public final BufferedReader inputReader(Charset charset) { * If the {@code native.encoding} is not a valid charset name or not supported * the {@link Charset#defaultCharset()} is used. * + *

The error reader should be {@linkplain BufferedReader#close closed} + * when it is no longer needed. + * * @return a {@link BufferedReader BufferedReader} using the * {@code native.encoding} if supported, otherwise, the * {@link Charset#defaultCharset()} @@ -314,10 +370,14 @@ public final BufferedReader errorReader() { * then the {@code InputStreamReader} will be reading from a * null input stream. * + *

The error reader should be {@linkplain BufferedReader#close closed} + * when it is no longer needed. + * * @apiNote - * Using both {@link #getErrorStream} and {@link #errorReader(Charset)} has - * unpredictable behavior since the buffered reader reads ahead from the - * error stream. + * Avoid using both {@link #getErrorStream} and {@link #errorReader(Charset)}. + * The error reader consumes and buffers bytes from the error stream. + * Bytes read from the error stream would not be seen by the reader and the + * buffer contents are unpredictable. * *

When the process has terminated, and the standard error has not been redirected, * reading of the bytes available from the underlying stream is on a best effort basis and @@ -354,6 +414,9 @@ public final BufferedReader errorReader(Charset charset) { * If the {@code native.encoding} is not a valid charset name or not supported * the {@link Charset#defaultCharset()} is used. * + *

The output writer should be {@linkplain BufferedWriter#close closed} + * when it is no longer needed. + * * @return a {@code BufferedWriter} to the standard input of the process using the charset * for the {@code native.encoding} system property * @since 17 @@ -383,6 +446,9 @@ public final BufferedWriter outputWriter() { * ProcessBuilder.redirectInput} then the {@code OutputStreamWriter} writes to a * null output stream. * + *

The output writer should be {@linkplain BufferedWriter#close closed} + * when it is no longer needed. + * * @apiNote * A {@linkplain BufferedWriter} writes characters, arrays of characters, and strings. * Wrapping the {@link BufferedWriter} with a {@link PrintWriter} provides @@ -674,11 +740,12 @@ public long pid() { * free the current thread and block only if and when the value is needed. *
* For example, launching a process to compare two files and get a boolean if they are identical: - *

 {@code   Process p = new ProcessBuilder("cmp", "f1", "f2").start();
-     *    Future identical = p.onExit().thenApply(p1 -> p1.exitValue() == 0);
-     *    ...
-     *    if (identical.get()) { ... }
-     * }
+ * {@snippet lang = "java" : + * Process p = new ProcessBuilder("cmp", "f1", "f2").start(); + * Future identical = p.onExit().thenApply(p1 -> p1.exitValue() == 0); + * ... + * if (identical.get()) { ... } + * } * * @implSpec * This implementation executes {@link #waitFor()} in a separate thread @@ -695,11 +762,11 @@ public long pid() { * External implementations should override this method and provide * a more efficient implementation. For example, to delegate to the underlying * process, it can do the following: - *
{@code
+     * {@snippet lang = "java" :
      *    public CompletableFuture onExit() {
      *       return delegate.onExit().thenApply(p -> this);
      *    }
-     * }
+ * } * @apiNote * The process may be observed to have terminated with {@link #isAlive} * before the ComputableFuture is completed and dependent actions are invoked. diff --git a/src/java.base/share/classes/java/lang/ProcessBuilder.java b/src/java.base/share/classes/java/lang/ProcessBuilder.java index 9cb5848bdff55..6bc15893518d3 100644 --- a/src/java.base/share/classes/java/lang/ProcessBuilder.java +++ b/src/java.base/share/classes/java/lang/ProcessBuilder.java @@ -150,30 +150,33 @@ *

Starting a new process which uses the default working directory * and environment is easy: * - *

 {@code
+ * {@snippet lang = "java" :
+
  * Process p = new ProcessBuilder("myCommand", "myArg").start();
- * }
+ * } + * *

Here is an example that starts a process with a modified working * directory and environment, and redirects standard output and error * to be appended to a log file: * - *

 {@code
- * ProcessBuilder pb =
- *   new ProcessBuilder("myCommand", "myArg1", "myArg2");
- * Map env = pb.environment();
- * env.put("VAR1", "myValue");
- * env.remove("OTHERVAR");
- * env.put("VAR2", env.get("VAR1") + "suffix");
- * pb.directory(new File("myDir"));
- * File log = new File("log");
- * pb.redirectErrorStream(true);
- * pb.redirectOutput(Redirect.appendTo(log));
- * Process p = pb.start();
- * assert pb.redirectInput() == Redirect.PIPE;
- * assert pb.redirectOutput().file() == log;
- * assert p.getInputStream().read() == -1;
- * }
+ * {@snippet lang = "java": + * ProcessBuilder pb = new ProcessBuilder("myCommand", "myArg1", "myArg2"); + * Map env = pb.environment(); + * env.put("VAR1", "myValue"); + * env.remove("OTHERVAR"); + * env.put("VAR2", env.get("VAR1") + "suffix"); + * + * pb.directory(new File("myDir")); + * File log = new File("log"); + * pb.redirectErrorStream(true); + * pb.redirectOutput(Redirect.appendTo(log)); + * + * Process p = pb.start(); + * assert pb.redirectInput() == Redirect.PIPE; + * assert pb.redirectOutput().file() == log; + * assert p.getInputStream().read() == -1; + * } * *

To start a process with an explicit set of environment * variables, first call {@link java.util.Map#clear() Map.clear()} @@ -506,10 +509,10 @@ public enum Type { * This is the default handling of subprocess standard I/O. * *

It will always be true that - *

 {@code
-         * Redirect.PIPE.file() == null &&
-         * Redirect.PIPE.type() == Redirect.Type.PIPE
-         * }
+ * {@snippet lang = "java" : + * Redirect.PIPE.file() == null && + * Redirect.PIPE.type() == Redirect.Type.PIPE + * } */ public static final Redirect PIPE = new Redirect() { public Type type() { return Type.PIPE; } @@ -521,10 +524,10 @@ public enum Type { * behavior of most operating system command interpreters (shells). * *

It will always be true that - *

 {@code
-         * Redirect.INHERIT.file() == null &&
-         * Redirect.INHERIT.type() == Redirect.Type.INHERIT
-         * }
+ * {@snippet lang = "java" : + * Redirect.INHERIT.file() == null && + * Redirect.INHERIT.type() == Redirect.Type.INHERIT + * } */ public static final Redirect INHERIT = new Redirect() { public Type type() { return Type.INHERIT; } @@ -537,11 +540,10 @@ public enum Type { * an operating system specific "null file". * *

It will always be true that - *

 {@code
-         * Redirect.DISCARD.file() is the filename appropriate for the operating system
-         * and may be null &&
-         * Redirect.DISCARD.type() == Redirect.Type.WRITE
-         * }
+ * {@snippet lang = "java" : + * Redirect.DISCARD.file() // is the filename appropriate for the operating system + * Redirect.DISCARD.type() == Redirect.Type.WRITE + * } * @since 9 */ public static final Redirect DISCARD = new Redirect() { @@ -572,10 +574,10 @@ boolean append() { * Returns a redirect to read from the specified file. * *

It will always be true that - *

 {@code
-         * Redirect.from(file).file() == file &&
-         * Redirect.from(file).type() == Redirect.Type.READ
-         * }
+ * {@snippet lang = "java" : + * Redirect.from(file).file() == file && + * Redirect.from(file).type() == Redirect.Type.READ + * } * * @param file The {@code File} for the {@code Redirect}. * @return a redirect to read from the specified file @@ -598,10 +600,10 @@ public String toString() { * its previous contents will be discarded. * *

It will always be true that - *

 {@code
-         * Redirect.to(file).file() == file &&
-         * Redirect.to(file).type() == Redirect.Type.WRITE
-         * }
+ * {@snippet lang = "java" : + * Redirect.to(file).file() == file && + * Redirect.to(file).type() == Redirect.Type.WRITE + * } * * @param file The {@code File} for the {@code Redirect}. * @return a redirect to write to the specified file @@ -628,10 +630,10 @@ public String toString() { * system-dependent and therefore unspecified. * *

It will always be true that - *

 {@code
-         * Redirect.appendTo(file).file() == file &&
-         * Redirect.appendTo(file).type() == Redirect.Type.APPEND
-         * }
+ * {@snippet lang = "java" : + * Redirect.appendTo(file).file() == file && + * Redirect.appendTo(file).type() == Redirect.Type.APPEND + * } * * @param file The {@code File} for the {@code Redirect}. * @return a redirect to append to the specified file @@ -914,15 +916,15 @@ public Redirect redirectError() { * to be the same as those of the current Java process. * *

This is a convenience method. An invocation of the form - *

 {@code
-     * pb.inheritIO()
-     * }
+ * {@snippet lang = "java" : + * pb.inheritIO() + * } * behaves in exactly the same way as the invocation - *
 {@code
-     * pb.redirectInput(Redirect.INHERIT)
-     *   .redirectOutput(Redirect.INHERIT)
-     *   .redirectError(Redirect.INHERIT)
-     * }
+ * {@snippet lang = "java" : + * pb.redirectInput(Redirect.INHERIT) + * .redirectOutput(Redirect.INHERIT) + * .redirectError(Redirect.INHERIT) + * } * * This gives behavior equivalent to most operating system * command interpreters, or the standard C library function @@ -1176,22 +1178,21 @@ private Process start(Redirect[] redirects) throws IOException { * @apiNote * For example to count the unique imports for all the files in a file hierarchy * on a Unix compatible platform: - *
{@code
-     * String directory = "/home/duke/src";
-     * ProcessBuilder[] builders = {
+     * {@snippet lang = "java" :
+     *     String directory = "/home/duke/src";
+     *     ProcessBuilder[] builders = {
      *              new ProcessBuilder("find", directory, "-type", "f"),
      *              new ProcessBuilder("xargs", "grep", "-h", "^import "),
      *              new ProcessBuilder("awk", "{print $2;}"),
      *              new ProcessBuilder("sort", "-u")};
-     * List processes = ProcessBuilder.startPipeline(
-     *         Arrays.asList(builders));
-     * Process last = processes.get(processes.size()-1);
-     * try (InputStream is = last.getInputStream();
+     *     List processes = ProcessBuilder.startPipeline( Arrays.asList(builders));
+     *     Process last = processes.get(processes.size() - 1);
+     *     try (InputStream is = last.getInputStream();
      *         Reader isr = new InputStreamReader(is);
      *         BufferedReader r = new BufferedReader(isr)) {
-     *     long count = r.lines().count();
+     *         long count = r.lines().count();
+     *     }
      * }
-     * }
* * @param builders a List of ProcessBuilders * @return a {@code List}es started from the corresponding