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.