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