ProcessBuilder: inoltro di stdout e stderr dei processi avviati senza bloccare il thread principale


93

Sto costruendo un processo in Java usando ProcessBuilder come segue:

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

InputStream stdOut = p.getInputStream();

Ora il mio problema è il seguente: vorrei catturare tutto ciò che sta attraversando stdout e / o stderr di quel processo e reindirizzarlo in System.outmodo asincrono. Voglio che il processo e il suo reindirizzamento dell'output vengano eseguiti in background. Finora, l'unico modo che ho trovato per farlo è generare manualmente un nuovo thread che leggerà continuamente stdOute quindi chiamerà il write()metodo appropriato di 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();

Anche se questo tipo di approccio funziona, sembra un po 'sporco. E per di più, mi dà un altro thread da gestire e terminare correttamente. C'è un modo migliore per farlo?


2
Se il blocco del thread chiamante fosse un'opzione, ci sarebbe una soluzione molto semplice anche in Java 6:org.apache.commons.io.IOUtils.copy(new ProcessBuilder().command(commandLine) .redirectErrorStream(true).start().getInputStream(), System.out);
oberlies

Risposte:


69

L'unico modo in Java 6 o versioni precedenti è con un cosiddetto StreamGobbler(che hai iniziato a creare):

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

Per Java 7, vedere la risposta di Evgeniy Dorofeev.


1
StreamGobbler catturerà tutto l'output? C'è qualche possibilità che manchi un po 'dell'output? Inoltre, il filo StreamGobbler morirà da solo una volta che il processo è fermo?
LordOfThePigs

1
Dal momento in cui InputStream è terminato, finirà anche.
asgoth

7
Per Java 6 e versioni precedenti, sembra che questa sia l'unica soluzione. Per java 7 e versioni successive, vedere l'altra risposta su ProcessBuilder.inheritIO ()
LordOfThePigs

@asgoth Esiste un modo per inviare input al processo? Ecco la mia domanda: stackoverflow.com/questions/28070841/… , sarò grato se qualcuno mi aiuterà a risolvere il problema.
DeepSidhu1313

144

Usa ProcessBuilder.inheritIO, imposta l'origine e la destinazione dell'I / O standard del sottoprocesso in modo che siano le stesse del processo Java corrente.

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

Se Java 7 non è un'opzione

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

I thread moriranno automaticamente al termine del sottoprocesso, perché srcsarà EOF.


1
Vedo che Java 7 ha aggiunto un sacco di metodi interessanti per la gestione di stdout, stderr e stdin. Molto carino. Penso che userò inheritIO()o uno di quei redirect*(ProcessBuilder.Redirect)metodi utili la prossima volta che avrò bisogno di farlo su un progetto Java 7. Purtroppo il mio progetto è java 6.
LordOfThePigs

Ah, OK ha aggiunto la mia versione 1.6
Evgeniy Dorofeev

scdeve essere chiuso?
hotohoto


Si noti che lo imposta sul descrittore di file del sistema operativo della JVM principale, non sui flussi System.out. Questo è quindi ok per scrivere su console o reindirizzamento shell del genitore, ma non funzionerà per i flussi di registrazione. Quelli hanno ancora bisogno di un thread pump (tuttavia puoi almeno reindirizzare stderr a stdin, quindi hai solo bisogno di un thread.
eckes

20

Una soluzione flessibile con Java 8 lambda che ti permette di fornire un Consumerche elaborerà l'output (es. Registrarlo) riga per riga. run()è un one-liner senza eccezioni controllate lanciate. In alternativa all'implementazione Runnable, può Threadinvece estendersi come suggeriscono altre risposte.

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

Puoi quindi usarlo ad esempio in questo modo:

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

Qui il flusso di output viene reindirizzato System.oute il flusso di errore viene registrato a livello di errore da logger.


Potresti espandere su come useresti questo?
Chris Turner

@Robert I thread si interromperanno automaticamente quando il flusso di input / errore corrispondente viene chiuso. Il forEach()nel run()metodo bloccherà finché il flusso è aperto, in attesa della riga successiva. Uscirà quando il flusso è chiuso.
Adam Michalik

13

È semplice come segue:

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

da .redirectErrorStream (true) si dice al processo di unire errore e flusso di output e quindi da .redirectOutput (file) si reindirizza l'output unito a un file.

Aggiornare:

Sono riuscito a farlo come segue:

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
}

Ora puoi vedere entrambi gli output dai thread principale e asyncOut in System.out


Questo non risponde alla domanda: vorrei catturare tutto ciò che sta attraversando stdout e / o stderr di quel processo e reindirizzarlo a System.out in modo asincrono. Voglio che il processo e il suo reindirizzamento dell'output vengano eseguiti in background.
Adam Michalik

@AdamMichalik, hai ragione - all'inizio non ho capito il succo. Grazie per aver mostrato.
nike.laos

Questo ha lo stesso problema di inheritIO (), scriverà sul poarent JVMs FD1 ma non su eventuali sostituzioni di System.out OutputStreams (come l'adattatore logger).
eckes il

3

Semplice soluzione java8 con l'acquisizione di entrambi gli output e l'elaborazione reattiva utilizzando 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 l'utilizzo:

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

Questa soluzione è molto semplice. Mostra anche indirettamente come reindirizzare, ad esempio, a un logger. Per un esempio dai un'occhiata alla mia risposta.
Keocra

3

C'è una libreria che fornisce un ProcessBuilder migliore, zt-exec. Questa libreria può fare esattamente quello che stai chiedendo e altro ancora.

Ecco come apparirebbe il tuo codice con zt-exec invece di ProcessBuilder:

aggiungi la dipendenza:

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

Il codice :

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

La documentazione della biblioteca è qui: https://github.com/zeroturnaround/zt-exec/


2

Anch'io posso usare solo Java 6. Ho usato l'implementazione dello scanner dei thread di @ EvgeniyDorofeev. Nel mio codice, al termine di un processo, devo eseguire immediatamente altri due processi che confrontano ciascuno l'output reindirizzato (uno unit test basato su diff per garantire che stdout e stderr siano gli stessi di quelli benedetti).

I thread dello scanner non finiscono abbastanza presto, anche se attendo () il completamento del processo. Per far funzionare correttamente il codice, devo assicurarmi che i thread siano uniti al termine del 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();
        }
    });
}

1

In aggiunta alla risposta di msangel, vorrei aggiungere il seguente blocco di codice:

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

Permette di reindirizzare il flusso di input (stdout, stderr) del processo a qualche altro consumatore. Potrebbe essere System.out :: println o qualsiasi altra cosa che utilizza stringhe.

Utilizzo:

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

Il tuo codice personalizzato va al posto del ...


-2

Per impostazione predefinita, il sottoprocesso creato non dispone di un proprio terminale o console. Tutte le sue operazioni I / O standard (cioè stdin, stdout, stderr) saranno reindirizzate al processo genitore, dove sarà possibile accedervi tramite i flussi ottenuti usando i metodi getOutputStream (), getInputStream () e getErrorStream (). Il processo genitore utilizza questi flussi per alimentare l'input e ottenere l'output dal sottoprocesso. Poiché alcune piattaforme native forniscono solo dimensioni del buffer limitate per flussi di input e output standard, la mancata scrittura tempestiva del flusso di input o la lettura del flusso di output del sottoprocesso può causare il blocco del sottoprocesso o addirittura un deadlock.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.