ProcessBuilder: пересылка stdout и stderr запущенных процессов без блокировки основного потока
Я создаю процесс на Java с помощью ProcessBuilder следующим образом:
ProcessBuilder pb = new ProcessBuilder()
.command("somecommand", "arg1", "arg2")
.redirectErrorStream(true);
Process p = pb.start();
InputStream stdOut = p.getInputStream();
Теперь моя проблема заключается в следующем: я хотел бы захватить все, что происходит через stdout и/или stderr этого процесса, и перенаправить его на System.out
асинхронно. Я хочу, чтобы процесс и его перенаправление вывода выполнялись в фоновом режиме. До сих пор единственный способ, которым я нашел это, - вручную создать новый поток, который будет непрерывно читать из stdOut
, а затем вызвать соответствующий метод write()
System.out
.
new Thread(new Runnable(){
public void run(){
byte[] buffer = new byte[8192];
int len = -1;
while((len = stdOut.read(buffer)) > 0){
System.out.write(buffer, 0, len);
}
}
}).start();
Пока этот подход работает, он чувствует себя немного грязным. И, кроме того, он дает мне еще один поток для правильного управления и завершения. Есть ли лучший способ сделать это?
Ответы
Ответ 1
Только в Java 6 или более ранней версии имеется так называемый StreamGobbler
(который вы начали создавать):
StreamGobbler errorGobbler = new StreamGobbler(p.getErrorStream(), "ERROR");
// any output?
StreamGobbler outputGobbler = new StreamGobbler(p.getInputStream(), "OUTPUT");
// start gobblers
outputGobbler.start();
errorGobbler.start();
...
private class StreamGobbler extends Thread {
InputStream is;
String type;
private StreamGobbler(InputStream is, String type) {
this.is = is;
this.type = type;
}
@Override
public void run() {
try {
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line = null;
while ((line = br.readLine()) != null)
System.out.println(type + "> " + line);
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
Для Java 7 см. ответ Евгения Дорофеева.
Ответ 2
Используйте ProcessBuilder.inheritIO
, он устанавливает источник и назначение для стандартного ввода-вывода подпроцесса таким же, как и для текущего процесса Java.
Process p = new ProcessBuilder().inheritIO().command("command1").start();
Если Java 7 не является опцией
public static void main(String[] args) throws Exception {
Process p = Runtime.getRuntime().exec("cmd /c dir");
inheritIO(p.getInputStream(), System.out);
inheritIO(p.getErrorStream(), System.err);
}
private static void inheritIO(final InputStream src, final PrintStream dest) {
new Thread(new Runnable() {
public void run() {
Scanner sc = new Scanner(src);
while (sc.hasNextLine()) {
dest.println(sc.nextLine());
}
}
}).start();
}
Нити будут автоматически умирать, когда подпроцесс завершается, потому что src
будет EOF.
Ответ 3
Гибкое решение с Java лямбдой, которое позволяет вам предоставить Consumer
, который будет обрабатывать выходные данные (например, log it) по очереди. run()
- однострочный, без отмеченных исключений. В качестве альтернативы реализации Runnable
он может расширить Thread
, как указывают другие ответы.
class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumeInputLine;
public StreamGobbler(InputStream inputStream, Consumer<String> consumeInputLine) {
this.inputStream = inputStream;
this.consumeInputLine = consumeInputLine;
}
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(consumeInputLine);
}
}
Затем вы можете использовать его, например, следующим образом:
public void runProcessWithGobblers() throws IOException, InterruptedException {
Process p = new ProcessBuilder("...").start();
Logger logger = LoggerFactory.getLogger(getClass());
StreamGobbler outputGobbler = new StreamGobbler(p.getInputStream(), System.out::println);
StreamGobbler errorGobbler = new StreamGobbler(p.getErrorStream(), logger::error);
new Thread(outputGobbler).start();
new Thread(errorGobbler).start();
p.waitFor();
}
Здесь выходной поток перенаправляется на System.out
, а поток ошибок регистрируется на уровне ошибки с помощью logger
.
Ответ 4
Это так просто, как показано ниже:
File logFile = new File(...);
ProcessBuilder pb = new ProcessBuilder()
.command("somecommand", "arg1", "arg2")
processBuilder.redirectErrorStream(true);
processBuilder.redirectOutput(logFile);
by.redirectErrorStream(true) вы сообщаете процессу ошибка слияния и поток вывода, а затем .redirectOutput(файл) вы перенаправляете объединенный вывод в файл.
Update:
Мне удалось это сделать следующим образом:
public static void main(String[] args) {
// Async part
Runnable r = () -> {
ProcessBuilder pb = new ProcessBuilder().command("...");
// Merge System.err and System.out
pb.redirectErrorStream(true);
// Inherit System.out as redirect output stream
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
try {
pb.start();
} catch (IOException e) {
e.printStackTrace();
}
};
new Thread(r, "asyncOut").start();
// here goes your main part
}
Теперь вы можете видеть оба выхода из основного и асинхронного потоков в System.out
Ответ 5
Я тоже могу использовать только Java 6. Я использовал реализацию потокового сканера @EvgeniyDorofeev. В моем коде после завершения процесса я должен немедленно выполнить два других процесса, каждый из которых сравнивает перенаправленный вывод (основанный на diff unit test, чтобы обеспечить, чтобы stdout и stderr были такими же, как и блаженные).
Потоки сканера не заканчиваются достаточно быстро, даже если я waitFor() процесс для завершения. Чтобы код работал правильно, я должен убедиться, что потоки соединяются после завершения процесса.
public static int runRedirect (String[] args, String stdout_redirect_to, String stderr_redirect_to) throws IOException, InterruptedException {
ProcessBuilder b = new ProcessBuilder().command(args);
Process p = b.start();
Thread ot = null;
PrintStream out = null;
if (stdout_redirect_to != null) {
out = new PrintStream(new BufferedOutputStream(new FileOutputStream(stdout_redirect_to)));
ot = inheritIO(p.getInputStream(), out);
ot.start();
}
Thread et = null;
PrintStream err = null;
if (stderr_redirect_to != null) {
err = new PrintStream(new BufferedOutputStream(new FileOutputStream(stderr_redirect_to)));
et = inheritIO(p.getErrorStream(), err);
et.start();
}
p.waitFor(); // ensure the process finishes before proceeding
if (ot != null)
ot.join(); // ensure the thread finishes before proceeding
if (et != null)
et.join(); // ensure the thread finishes before proceeding
int rc = p.exitValue();
return rc;
}
private static Thread inheritIO (final InputStream src, final PrintStream dest) {
return new Thread(new Runnable() {
public void run() {
Scanner sc = new Scanner(src);
while (sc.hasNextLine())
dest.println(sc.nextLine());
dest.flush();
}
});
}
Ответ 6
Простое решение java8 с захватом обоих выходов и реактивной обработкой с использованием CompletableFuture
:
static CompletableFuture<String> readOutStream(InputStream is) {
return CompletableFuture.supplyAsync(() -> {
try (
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
){
StringBuilder res = new StringBuilder();
String inputLine;
while ((inputLine = br.readLine()) != null) {
res.append(inputLine).append(System.lineSeparator());
}
return res.toString();
} catch (Throwable e) {
throw new RuntimeException("problem with executing program", e);
}
});
}
И использование:
Process p = Runtime.getRuntime().exec(cmd);
CompletableFuture<String> soutFut = readOutStream(p.getInputStream());
CompletableFuture<String> serrFut = readOutStream(p.getErrorStream());
CompletableFuture<String> resultFut = soutFut.thenCombine(serrFut, (stdout, stderr) -> {
// print to current stderr the stderr of process and return the stdout
System.err.println(stderr);
return stdout;
});
// get stdout once ready, blocking
String result = resultFut.get();
Ответ 7
Thread thread = new Thread(() -> {
new BufferedReader(
new InputStreamReader(inputStream,
StandardCharsets.UTF_8))
.lines().forEach(...);
});
thread.start();
Ваш пользовательский код идет вместо ...
Ответ 8
По умолчанию созданный подпроцесс не имеет собственного терминала или консоли. Все его стандартные операции ввода-вывода (т.е. Stdin, stdout, stderr) будут перенаправлены на родительский процесс, где к ним можно получить доступ через потоки, полученные с помощью методов getOutputStream(), getInputStream() и getErrorStream(). Родительский процесс использует эти потоки для подачи ввода и получения вывода из подпроцесса. Поскольку некоторые собственные платформы обеспечивают ограниченный размер буфера для стандартных потоков ввода и вывода, неспособность быстро записать входной поток или прочитать выходной поток подпроцесса, может привести к блокировке подпроцесса или даже к взаимоблокировке.
https://www.securecoding.cert.org/confluence/display/java/FIO07-J.+Do+not+let+external+processes+block+on+IO+buffers