ProcessBuilder: Encaminhando stdout e stderr de processos iniciados sem bloquear o thread principal

93

Estou construindo um processo em Java usando ProcessBuilder da seguinte maneira:

ProcessBuilder pb = new ProcessBuilder()
        .command("somecommand", "arg1", "arg2")
        .redirectErrorStream(true);
Process p = pb.start();

InputStream stdOut = p.getInputStream();

Agora, meu problema é o seguinte: Eu gostaria de capturar tudo o que está passando por stdout e / ou stderr desse processo e redirecioná-lo de System.outforma assíncrona. Quero que o processo e seu redirecionamento de saída sejam executados em segundo plano. Até agora, a única maneira que encontrei de fazer isso é gerar manualmente um novo thread que lerá continuamente stdOute chamará o write()método apropriado de 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();

Embora essa abordagem funcione, parece um pouco suja. E, além disso, me dá mais um thread para gerenciar e encerrar corretamente. Existe alguma maneira melhor de fazer isso?

LordOfThePigs
fonte
2
Se bloquear o thread de chamada fosse uma opção, haveria uma solução muito simples mesmo em Java 6:org.apache.commons.io.IOUtils.copy(new ProcessBuilder().command(commandLine) .redirectErrorStream(true).start().getInputStream(), System.out);
oberlies

Respostas:

69

A única maneira no Java 6 ou anterior é com um assim chamado StreamGobbler(que você começou a criar):

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();
        }
    }
}

Para Java 7, consulte a resposta de Evgeniy Dorofeev.

asgoth
fonte
1
O StreamGobbler pegará toda a saída? Existe alguma chance de perder parte da saída? Além disso, o thread do StreamGobbler morrerá sozinho quando o processo parar?
LordOfThePigs
1
A partir do momento em que o InputStream terminou, ele também terminará.
asgoth
7
Para Java 6 e anteriores, parece que esta é a única solução. Para java 7 e superior, consulte a outra resposta sobre ProcessBuilder.inheritIO ()
LordOfThePigs
@asgoth Existe alguma maneira de enviar entrada para o processo? Aqui está a minha pergunta: stackoverflow.com/questions/28070841/… , ficarei grato se alguém me ajudar a resolver o problema.
DeepSidhu1313
144

Use ProcessBuilder.inheritIO, ele define a origem e o destino para E / S padrão de subprocesso como sendo os mesmos do processo Java atual.

Process p = new ProcessBuilder().inheritIO().command("command1").start();

Se Java 7 não for uma opção

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();
}

Threads morrerão automaticamente quando o subprocesso terminar, porque srcEOF.

Evgeniy Dorofeev
fonte
1
Vejo que o Java 7 adicionou vários métodos interessantes para lidar com stdout, stderr e stdin. Muito bom. Acho que vou usar inheritIO()um desses redirect*(ProcessBuilder.Redirect)métodos úteis na próxima vez que precisar fazer isso em um projeto java 7. Infelizmente, meu projeto é java 6.
LordOfThePigs
Ah, OK adicionou minha versão 1.6
Evgeniy Dorofeev
scprecisa ser fechado?
hotohoto
você pode ajudar com stackoverflow.com/questions/43051640/… ?
gstackoverflow
Observe que ele o define para o filedescriptor do SO da JVM pai, não para os fluxos System.out. Portanto, não há problema em gravar no console ou no redirecionamento do shell do pai, mas não funcionará para fluxos de registro. Aqueles ainda precisam de um thread de pump (no entanto, você pode pelo menos redirecionar stderr para stdin, então você só precisa de um thread.
eckes
20

Uma solução flexível com lambda Java 8 que permite fornecer um Consumerque processará a saída (por exemplo, registrá-la) linha por linha. run()é um one-liner sem exceções verificadas lançadas. Como alternativa à implementação Runnable, ele pode se estender, Threadcomo outras respostas sugerem.

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);
    }
}

Você pode usá-lo, por exemplo, desta forma:

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();
}

Aqui, o fluxo de saída é redirecionado para System.oute o fluxo de erro é registrado no nível de erro pelo logger.

Adam Michalik
fonte
Você poderia expandir como usar isso?
Chris Turner
@Robert Os threads irão parar automaticamente quando o fluxo de entrada / erro correspondente for fechado. O forEach()no run()método irá bloquear até que o fluxo seja aberto, aguardando a próxima linha. Ele sairá quando o fluxo for fechado.
Adam Michalik
13

É tão simples quanto seguir:

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

por .redirectErrorStream (true) você diz ao processo para mesclar erro e fluxo de saída e, em seguida, por .redirectOutput (arquivo), você redireciona a saída mesclada para um arquivo.

Atualizar:

Eu consegui fazer isso da seguinte maneira:

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
}

Agora você pode ver as duas saídas dos threads principal e asyncOut em System.out

nike.laos
fonte
Isso não responde à pergunta: eu gostaria de capturar tudo o que está passando por stdout e / ou stderr desse processo e redirecioná-lo para System.out de forma assíncrona. Quero que o processo e seu redirecionamento de saída sejam executados em segundo plano.
Adam Michalik
@AdamMichalik, você está certo - eu não entendi a essência, no começo. Obrigado por aparecer.
nike.laos
Isso tem o mesmo problema que o inheritIO (), ele gravará nas JVMs FD1, mas não em qualquer substituto System.out OutputStreams (como o adaptador de log).
eckes
3

Solução java8 simples com captura de saídas e processamento reativo usando 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);
        }
    });
}

E o uso:

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();
msangel
fonte
Esta solução é muito simples. Também mostra indiretamente como redirecionar, por exemplo, para um logger. Por exemplo, dê uma olhada na minha resposta.
Keocra
3

Existe uma biblioteca que fornece um ProcessBuilder melhor, zt-exec. Esta biblioteca pode fazer exatamente o que você está pedindo e muito mais.

Aqui está como seu código ficaria com zt-exec em vez de ProcessBuilder:

adicione a dependência:

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

O código :

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

A documentação da biblioteca está aqui: https://github.com/zeroturnaround/zt-exec/

mryan
fonte
2

Eu também posso usar apenas Java 6. Usei a implementação do scanner de thread de @EvgeniyDorofeev. No meu código, depois que um processo termina, tenho que executar imediatamente dois outros processos, cada um comparando a saída redirecionada (um teste de unidade baseado em diff para garantir que stdout e stderr são iguais aos abençoados).

Os threads do scanner não terminam logo o suficiente, mesmo se eu esperar () que o processo seja concluído. Para fazer o código funcionar corretamente, tenho que garantir que os threads sejam unidos após o término do processo.

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();
        }
    });
}
Jeff Holt
fonte
1

Como complemento à resposta do msangel , gostaria de adicionar o seguinte bloco de código:

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;
            }
        });
    }

Ele permite redirecionar o fluxo de entrada (stdout, stderr) do processo para algum outro consumidor. Isso pode ser System.out :: println ou qualquer outra coisa que consuma strings.

Uso:

...
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());
Keocra
fonte
0
Thread thread = new Thread(() -> {
      new BufferedReader(
          new InputStreamReader(inputStream, 
                                StandardCharsets.UTF_8))
              .lines().forEach(...);
    });
    thread.start();

Seu código personalizado vai em vez do ...

Máxima
fonte
-2

Por padrão, o subprocesso criado não possui seu próprio terminal ou console. Todas as suas operações de E / S padrão (isto é, stdin, stdout, stderr) serão redirecionadas para o processo pai, onde podem ser acessadas através dos fluxos obtidos usando os métodos getOutputStream (), getInputStream () e getErrorStream (). O processo pai usa esses fluxos para alimentar a entrada e obter a saída do subprocesso. Como algumas plataformas nativas fornecem apenas um tamanho de buffer limitado para fluxos de entrada e saída padrão, a falha em gravar prontamente o fluxo de entrada ou ler o fluxo de saída do subprocesso pode causar o bloqueio do subprocesso ou até mesmo um impasse.

https://www.securecoding.cert.org/confluence/display/java/FIO07-J.+Do+not+let+external+processes+block+on+IO+buffers

sonal kumar sinha
fonte