ProcessBuilder: пересылка stdout и stderr запущенных процессов без блокировки основного потока

93

Я создаю процесс на 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();

Хотя такой подход работает, он кажется немного грязным. И вдобавок ко всему, это дает мне еще один поток для правильного управления и завершения. Есть ли лучший способ сделать это?

LordOfThePigs
источник
3
Если бы можно было заблокировать вызывающий поток, то даже в Java 6 было бы очень простое решение:org.apache.commons.io.IOUtils.copy(new ProcessBuilder().command(commandLine) .redirectErrorStream(true).start().getInputStream(), System.out);
oberlies

Ответы:

69

Единственный способ в 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 см. Ответ Евгения Дорофеева.

Асгот
источник
1
Будет ли StreamGobbler перехватывать весь вывод? Есть ли шанс, что он упустил какой-то результат? Кроме того, поток StreamGobbler умрет сам по себе, когда процесс остановится?
LordOfThePigs 04
1
С того момента, как закончился InputStream, он тоже закончится.
asgoth 04
7
Для Java 6 и более ранних версий это единственное решение. Для Java 7 и выше см. Другой ответ о ProcessBuilder.inheritIO ()
LordOfThePigs
@asgoth Есть ли способ отправить ввод в процесс? Вот мой вопрос: stackoverflow.com/questions/28070841/… , буду признателен, если кто-то поможет мне решить проблему.
DeepSidhu1313,
145

При использовании 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.

Евгений Дорофеев
источник
1
Я вижу, что в Java 7 добавлено несколько интересных методов для обработки stdout, stderr и stdin. Довольно мило. Думаю, я воспользуюсь inheritIO()одним из этих удобных redirect*(ProcessBuilder.Redirect)методов в следующий раз, когда мне понадобится сделать это в проекте java 7. К сожалению, мой проект - java 6.
LordOfThePigs
Ах, ОК, добавил мою версию 1.6
Евгений Дорофеев
scнужно закрыть?
hotohoto
вы можете помочь с stackoverflow.com/questions/43051640/… ?
gstackoverflow
Обратите внимание, что он устанавливает его в файловый дескриптор ОС родительской JVM, а не в потоки System.out. Таким образом, это нормально для записи в консоль или перенаправления оболочки родительского элемента, но это не будет работать для протоколирования потоков. Им по-прежнему нужен поток насоса (однако вы можете хотя бы перенаправить stderr на stdin, поэтому вам нужен только один поток.
eckes
20

Гибкое решение с лямбда- Consumerвыражением Java 8, которое позволяет вам предоставить объект, который будет обрабатывать вывод (например, регистрировать его) построчно. 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.

Адам Михалик
источник
Не могли бы вы рассказать, как бы вы это использовали?
Крис Тернер,
@Robert Потоки остановятся автоматически, когда соответствующий поток ввода / ошибки будет закрыт. Метод forEach()в run()методе будет блокироваться до тех пор, пока поток не откроется, ожидая следующей строки. Он выйдет, когда поток будет закрыт.
Адам Михалик
16

Это очень просто:

    File logFile = new File(...);
    ProcessBuilder pb = new ProcessBuilder()
        .command("somecommand", "arg1", "arg2")
    processBuilder.redirectErrorStream(true);
    processBuilder.redirectOutput(logFile);

с помощью .redirectErrorStream (true) вы сообщаете процессу объединить ошибку и выходной поток, а затем с помощью .redirectOutput (файл) перенаправляете объединенный вывод в файл.

Обновить:

Мне удалось сделать это следующим образом:

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
}

Теперь вы можете видеть оба вывода из основного и asyncOut потоков в System.out

nike.laos
источник
Это не отвечает на вопрос: я хотел бы захватить все, что проходит через stdout и / или stderr этого процесса, и асинхронно перенаправить его в System.out. Я хочу, чтобы процесс и его перенаправление вывода выполнялись в фоновом режиме.
Адам Михалик
@AdamMichalik, ты прав - я сначала не понял сути. Спасибо за показ.
nike.laos
У него та же проблема, что и у inheritIO (), он будет записывать на родительские JVM FD1, но не на какие-либо заменяющие System.out OutputStreams (например, адаптер регистратора).
eckes
4

Простое решение 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();
мсангел
источник
Это решение очень простое. Он также косвенно показывает, как перенаправить, например, на регистратор. Для примера взгляните на мой ответ.
keocra
3

Есть библиотека, которая предоставляет лучший ProcessBuilder, zt-exec. Эта библиотека может делать именно то, о чем вы просите, и даже больше.

Вот как будет выглядеть ваш код с zt-exec вместо ProcessBuilder:

добавить зависимость:

<dependency>
  <groupId>org.zeroturnaround</groupId>
  <artifactId>zt-exec</artifactId>
  <version>1.11</version>
</dependency>

Код :

new ProcessExecutor()
  .command("somecommand", "arg1", "arg2")
  .redirectOutput(System.out)
  .redirectError(System.err)
  .execute();

Документация библиотеки находится здесь: https://github.com/zeroturnaround/zt-exec/

Мрайан
источник
2

Я тоже могу использовать только Java 6. Я использовал реализацию сканера потоков @ EvgeniyDorofeev. В моем коде после завершения процесса я должен немедленно выполнить два других процесса, каждый из которых сравнивает перенаправленный вывод (модульный тест на основе diff, чтобы убедиться, что stdout и stderr такие же, как и благословенные).

Потоки сканера не завершаются достаточно быстро, даже если я жду завершения процесса (). Чтобы код работал правильно, я должен убедиться, что потоки соединяются после завершения процесса.

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();
        }
    });
}
Джефф Холт
источник
1

В дополнение к ответу msangel я хотел бы добавить следующий блок кода:

private static CompletableFuture<Boolean> redirectToLogger(final InputStream inputStream, final Consumer<String> logLineConsumer) {
        return CompletableFuture.supplyAsync(() -> {
            try (
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            ) {
                String line = null;
                while((line = bufferedReader.readLine()) != null) {
                    logLineConsumer.accept(line);
                }
                return true;
            } catch (IOException e) {
                return false;
            }
        });
    }

Он позволяет перенаправить входной поток (stdout, stderr) процесса другому потребителю. Это может быть System.out :: println или что-то еще, использующее строки.

Применение:

...
Process process = processBuilder.start()
CompletableFuture<Boolean> stdOutRes = redirectToLogger(process.getInputStream(), System.out::println);
CompletableFuture<Boolean> stdErrRes = redirectToLogger(process.getErrorStream(), System.out::println);
System.out.println(stdOutRes.get());
System.out.println(stdErrRes.get());
System.out.println(process.waitFor());
Кеокра
источник
0
Thread thread = new Thread(() -> {
      new BufferedReader(
          new InputStreamReader(inputStream, 
                                StandardCharsets.UTF_8))
              .lines().forEach(...);
    });
    thread.start();

Ваш собственный код используется вместо ...

Максим
источник
-1
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Main {

    public static void main(String[] args) throws Exception {
        ProcessBuilder pb = new ProcessBuilder("script.bat");
        pb.redirectErrorStream(true);
        Process p = pb.start();
        BufferedReader logReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
        String logLine = null;
        while ( (logLine = logReader.readLine()) != null) {
           System.out.println("Script output: " + logLine);
        }
    }
}

Используя эту строку: pb.redirectErrorStream(true);мы можем объединить InputStream и ErrorStream

Склимкович
источник
Это буквально уже используется в коде, о котором идет речь ...
LordOfThePigs,
-2

По умолчанию созданный подпроцесс не имеет собственного терминала или консоли. Все его стандартные операции ввода-вывода (т.е. 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

Сонал Кумар Синха
источник