mirror of
https://github.com/openjdk/jdk.git
synced 2026-03-13 17:33:10 +00:00
8354872: Clarify java.lang.Process resource cleanup
Reviewed-by: iris Backport-of: afb4a1be9e5dc2a9c0d812f5a36717c9f82241a9
This commit is contained in:
parent
9a73987f9b
commit
3e93b98baf
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 1995, 2024, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 1995, 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
|
||||
@ -41,7 +41,7 @@ import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* {@code Process} provides control of native processes started by
|
||||
* ProcessBuilder.start and Runtime.exec.
|
||||
* {@code ProcessBuilder.start} and {@code Runtime.exec}.
|
||||
* The class provides methods for performing input from the process, performing
|
||||
* output to the process, waiting for the process to complete,
|
||||
* checking the exit status of the process, and destroying (killing)
|
||||
@ -78,10 +78,6 @@ import java.util.stream.Stream;
|
||||
* process I/O can also be redirected</a>
|
||||
* using methods of the {@link ProcessBuilder} class.
|
||||
*
|
||||
* <p>The process is not killed when there are no more references to
|
||||
* the {@code Process} object, but rather the process
|
||||
* continues executing asynchronously.
|
||||
*
|
||||
* <p>There is no requirement that the process represented by a {@code
|
||||
* Process} object execute asynchronously or concurrently with respect
|
||||
* to the Java process that owns the {@code Process} object.
|
||||
@ -98,6 +94,49 @@ import java.util.stream.Stream;
|
||||
* Delegating to the underlying Process or ProcessHandle is typically
|
||||
* easiest and most efficient.
|
||||
*
|
||||
* <h2>Resource Usage</h2>
|
||||
* {@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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
* <p>For example, to capture the output of a program known to produce some output and then exit:
|
||||
* {@snippet lang = "java" :
|
||||
* List<String> capture(List<String> args) throws Exception {
|
||||
* ProcessBuilder pb = new ProcessBuilder(args);
|
||||
* Process process = pb.start();
|
||||
* try (BufferedReader in = process.inputReader()) {
|
||||
* List<String> captured = in.readAllLines();
|
||||
* int status = process.waitFor();
|
||||
* if (status != 0) {
|
||||
* throw new RuntimeException("Process %d: %s failed with %d"
|
||||
* .formatted(process.pid(), args, status));
|
||||
* }
|
||||
* return captured;
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* <p>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. The closing of the stream typically results in the other process exiting.
|
||||
*
|
||||
* <p> {@linkplain #destroy Destroying a process} signals the operating system to terminate the process.
|
||||
* It is up to the operating system to clean up and release the resources of that process.
|
||||
* Typically, file descriptors and handles are closed. When they are closed, any connections to
|
||||
* other processes are terminated and file descriptors and handles in the invoking process signal
|
||||
* end-of-file or closed. Usually, that is seen as an end-of-file or an exception.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public abstract class Process {
|
||||
@ -127,6 +166,9 @@ public abstract class Process {
|
||||
* then this method will return a
|
||||
* <a href="ProcessBuilder.html#redirect-input">null output stream</a>.
|
||||
*
|
||||
* <p>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 +201,15 @@ public abstract class Process {
|
||||
* then the input stream returned by this method will receive the
|
||||
* merged standard output and the standard error of the process.
|
||||
*
|
||||
* <p>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.
|
||||
* Use either this method or an {@linkplain #inputReader() input reader}
|
||||
* but not both on the same {@code Process}.
|
||||
* 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 +233,15 @@ public abstract class Process {
|
||||
* then this method will return a
|
||||
* <a href="ProcessBuilder.html#redirect-output">null input stream</a>.
|
||||
*
|
||||
* <p>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.
|
||||
* Use either this method or an {@linkplain #errorReader() error reader}
|
||||
* but not both on the same {@code Process}.
|
||||
* 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 +262,16 @@ public abstract class Process {
|
||||
* If the {@code native.encoding} is not a valid charset name or not supported
|
||||
* the {@link Charset#defaultCharset()} is used.
|
||||
*
|
||||
* <p>The reader should be {@linkplain BufferedReader#close closed}
|
||||
* when it is no longer needed.
|
||||
*
|
||||
* @apiNote
|
||||
* Use either this method or the {@linkplain #getInputStream input stream}
|
||||
* but not both on the same {@code Process}.
|
||||
* 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.
|
||||
*
|
||||
* @return a {@link BufferedReader BufferedReader} using the
|
||||
* {@code native.encoding} if supported, otherwise, the
|
||||
* {@link Charset#defaultCharset()}
|
||||
@ -238,6 +302,9 @@ public abstract class Process {
|
||||
* then the {@code InputStreamReader} will be reading from a
|
||||
* <a href="ProcessBuilder.html#redirect-output">null input stream</a>.
|
||||
*
|
||||
* <p>The reader should be {@linkplain BufferedReader#close closed}
|
||||
* when it is no longer needed.
|
||||
*
|
||||
* <p>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 +312,11 @@ public abstract class Process {
|
||||
* 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.
|
||||
* Use either this method or the {@linkplain #getInputStream input stream}
|
||||
* but not both on the same {@code Process}.
|
||||
* 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.
|
||||
*
|
||||
* <p>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 +352,16 @@ public abstract class Process {
|
||||
* If the {@code native.encoding} is not a valid charset name or not supported
|
||||
* the {@link Charset#defaultCharset()} is used.
|
||||
*
|
||||
* <p>The error reader should be {@linkplain BufferedReader#close closed}
|
||||
* when it is no longer needed.
|
||||
*
|
||||
* @apiNote
|
||||
* Use either this method or the {@linkplain #getErrorStream error stream}
|
||||
* but not both on the same {@code Process}.
|
||||
* 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.
|
||||
*
|
||||
* @return a {@link BufferedReader BufferedReader} using the
|
||||
* {@code native.encoding} if supported, otherwise, the
|
||||
* {@link Charset#defaultCharset()}
|
||||
@ -314,10 +393,15 @@ public abstract class Process {
|
||||
* then the {@code InputStreamReader} will be reading from a
|
||||
* <a href="ProcessBuilder.html#redirect-output">null input stream</a>.
|
||||
*
|
||||
* <p>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.
|
||||
* Use either this method or the {@linkplain #getErrorStream error stream}
|
||||
* but not both on the same {@code Process}.
|
||||
* 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.
|
||||
*
|
||||
* <p>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
|
||||
@ -346,7 +430,7 @@ public abstract class Process {
|
||||
/**
|
||||
* Returns a {@code BufferedWriter} connected to the normal input of the process
|
||||
* using the native encoding.
|
||||
* Writes text to a character-output stream, buffering characters so as to provide
|
||||
* Writes text to a character-output stream, buffering characters to provide
|
||||
* for the efficient writing of single characters, arrays, and strings.
|
||||
*
|
||||
* <p>This method delegates to {@link #outputWriter(Charset)} using the
|
||||
@ -354,6 +438,9 @@ public abstract class Process {
|
||||
* If the {@code native.encoding} is not a valid charset name or not supported
|
||||
* the {@link Charset#defaultCharset()} is used.
|
||||
*
|
||||
* <p>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
|
||||
@ -365,7 +452,7 @@ public abstract class Process {
|
||||
/**
|
||||
* Returns a {@code BufferedWriter} connected to the normal input of the process
|
||||
* using a Charset.
|
||||
* Writes text to a character-output stream, buffering characters so as to provide
|
||||
* Writes text to a character-output stream, buffering characters to provide
|
||||
* for the efficient writing of single characters, arrays, and strings.
|
||||
*
|
||||
* <p>Characters written by the writer are encoded to bytes using {@link OutputStreamWriter}
|
||||
@ -383,6 +470,9 @@ public abstract class Process {
|
||||
* ProcessBuilder.redirectInput} then the {@code OutputStreamWriter} writes to a
|
||||
* <a href="ProcessBuilder.html#redirect-input">null output stream</a>.
|
||||
*
|
||||
* <p>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 +764,12 @@ public abstract class Process {
|
||||
* free the current thread and block only if and when the value is needed.
|
||||
* <br>
|
||||
* For example, launching a process to compare two files and get a boolean if they are identical:
|
||||
* <pre> {@code Process p = new ProcessBuilder("cmp", "f1", "f2").start();
|
||||
* Future<Boolean> identical = p.onExit().thenApply(p1 -> p1.exitValue() == 0);
|
||||
* ...
|
||||
* if (identical.get()) { ... }
|
||||
* }</pre>
|
||||
* {@snippet lang = "java" :
|
||||
* Process p = new ProcessBuilder("cmp", "f1", "f2").start();
|
||||
* Future<Boolean> identical = p.onExit().thenApply(p1 -> p1.exitValue() == 0);
|
||||
* ...
|
||||
* if (identical.get()) { ... }
|
||||
* }
|
||||
*
|
||||
* @implSpec
|
||||
* This implementation executes {@link #waitFor()} in a separate thread
|
||||
@ -695,11 +786,11 @@ public abstract class Process {
|
||||
* 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:
|
||||
* <pre>{@code
|
||||
* {@snippet lang = "java" :
|
||||
* public CompletableFuture<Process> onExit() {
|
||||
* return delegate.onExit().thenApply(p -> this);
|
||||
* }
|
||||
* }</pre>
|
||||
* }
|
||||
* @apiNote
|
||||
* The process may be observed to have terminated with {@link #isAlive}
|
||||
* before the ComputableFuture is completed and dependent actions are invoked.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2003, 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
|
||||
@ -150,30 +150,33 @@ import jdk.internal.event.ProcessStartEvent;
|
||||
* <p>Starting a new process which uses the default working directory
|
||||
* and environment is easy:
|
||||
*
|
||||
* <pre> {@code
|
||||
* {@snippet lang = "java" :
|
||||
|
||||
* Process p = new ProcessBuilder("myCommand", "myArg").start();
|
||||
* }</pre>
|
||||
* }
|
||||
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <pre> {@code
|
||||
* ProcessBuilder pb =
|
||||
* new ProcessBuilder("myCommand", "myArg1", "myArg2");
|
||||
* Map<String, String> 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;
|
||||
* }</pre>
|
||||
* {@snippet lang = "java":
|
||||
* ProcessBuilder pb = new ProcessBuilder("myCommand", "myArg1", "myArg2");
|
||||
* Map<String, String> 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;
|
||||
* }
|
||||
*
|
||||
* <p>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 final class ProcessBuilder
|
||||
* This is the default handling of subprocess standard I/O.
|
||||
*
|
||||
* <p>It will always be true that
|
||||
* <pre> {@code
|
||||
* Redirect.PIPE.file() == null &&
|
||||
* Redirect.PIPE.type() == Redirect.Type.PIPE
|
||||
* }</pre>
|
||||
* {@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 final class ProcessBuilder
|
||||
* behavior of most operating system command interpreters (shells).
|
||||
*
|
||||
* <p>It will always be true that
|
||||
* <pre> {@code
|
||||
* Redirect.INHERIT.file() == null &&
|
||||
* Redirect.INHERIT.type() == Redirect.Type.INHERIT
|
||||
* }</pre>
|
||||
* {@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 final class ProcessBuilder
|
||||
* an operating system specific "null file".
|
||||
*
|
||||
* <p>It will always be true that
|
||||
* <pre> {@code
|
||||
* Redirect.DISCARD.file() is the filename appropriate for the operating system
|
||||
* and may be null &&
|
||||
* Redirect.DISCARD.type() == Redirect.Type.WRITE
|
||||
* }</pre>
|
||||
* {@snippet lang = "java" :
|
||||
* Redirect.DISCARD.file() != null && // 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 @@ public final class ProcessBuilder
|
||||
* Returns a redirect to read from the specified file.
|
||||
*
|
||||
* <p>It will always be true that
|
||||
* <pre> {@code
|
||||
* Redirect.from(file).file() == file &&
|
||||
* Redirect.from(file).type() == Redirect.Type.READ
|
||||
* }</pre>
|
||||
* {@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 final class ProcessBuilder
|
||||
* its previous contents will be discarded.
|
||||
*
|
||||
* <p>It will always be true that
|
||||
* <pre> {@code
|
||||
* Redirect.to(file).file() == file &&
|
||||
* Redirect.to(file).type() == Redirect.Type.WRITE
|
||||
* }</pre>
|
||||
* {@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 final class ProcessBuilder
|
||||
* system-dependent and therefore unspecified.
|
||||
*
|
||||
* <p>It will always be true that
|
||||
* <pre> {@code
|
||||
* Redirect.appendTo(file).file() == file &&
|
||||
* Redirect.appendTo(file).type() == Redirect.Type.APPEND
|
||||
* }</pre>
|
||||
* {@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 final class ProcessBuilder
|
||||
* to be the same as those of the current Java process.
|
||||
*
|
||||
* <p>This is a convenience method. An invocation of the form
|
||||
* <pre> {@code
|
||||
* pb.inheritIO()
|
||||
* }</pre>
|
||||
* {@snippet lang = "java" :
|
||||
* pb.inheritIO()
|
||||
* }
|
||||
* behaves in exactly the same way as the invocation
|
||||
* <pre> {@code
|
||||
* pb.redirectInput(Redirect.INHERIT)
|
||||
* .redirectOutput(Redirect.INHERIT)
|
||||
* .redirectError(Redirect.INHERIT)
|
||||
* }</pre>
|
||||
* {@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 @@ public final class ProcessBuilder
|
||||
* @apiNote
|
||||
* For example to count the unique imports for all the files in a file hierarchy
|
||||
* on a Unix compatible platform:
|
||||
* <pre>{@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<Process> processes = ProcessBuilder.startPipeline(
|
||||
* Arrays.asList(builders));
|
||||
* Process last = processes.get(processes.size()-1);
|
||||
* try (InputStream is = last.getInputStream();
|
||||
* List<Process> 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();
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @param builders a List of ProcessBuilders
|
||||
* @return a {@code List<Process>}es started from the corresponding
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user