ProcessBuilder: Weiterleiten von stdout und stderr von gestarteten Prozessen, ohne den Hauptthread zu blockieren


93

Ich erstelle einen Prozess in Java mit ProcessBuilder wie folgt:

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

InputStream stdOut = p.getInputStream();

Jetzt ist mein Problem das Folgende: Ich möchte erfassen, was durch stdout und / oder stderr dieses Prozesses läuft, und es System.outasynchron umleiten . Ich möchte, dass der Prozess und seine Ausgabeumleitung im Hintergrund ausgeführt werden. Bisher habe ich nur die Möglichkeit gefunden, manuell einen neuen Thread zu erstellen, der kontinuierlich aus liest stdOutund dann die entsprechende write()Methode von aufruft 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();

Während dieser Ansatz funktioniert, fühlt er sich ein bisschen schmutzig an. Und obendrein gibt es mir noch einen Thread, den ich richtig verwalten und beenden kann. Gibt es einen besseren Weg, dies zu tun?


2
Wenn das Blockieren des aufrufenden Threads eine Option wäre, gäbe es sogar in Java 6 eine sehr einfache Lösung:org.apache.commons.io.IOUtils.copy(new ProcessBuilder().command(commandLine) .redirectErrorStream(true).start().getInputStream(), System.out);
oberlies

Antworten:


69

Nur in Java 6 oder früher gibt es einen so genannten StreamGobbler(den Sie gerade erstellen):

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

Für Java 7 siehe Evgeniy Dorofeevs Antwort.


1
Fängt der StreamGobbler die gesamte Ausgabe ab? Gibt es eine Chance, dass ein Teil der Ausgabe fehlt? Wird der StreamGobbler-Thread auch von selbst sterben, sobald der Prozess gestoppt ist?
LordOfThePigs

1
Sobald der InputStream beendet ist, endet er auch.
Asgoth

7
Für Java 6 und frühere Versionen scheint dies die einzige Lösung zu sein. Für Java 7 und
höher

@asgoth Gibt es eine Möglichkeit, Eingaben an den Prozess zu senden? Hier ist meine Frage: stackoverflow.com/questions/28070841/… , ich bin dankbar, wenn mir jemand bei der Lösung des Problems hilft.
DeepSidhu1313

144

Bei Verwendung ProcessBuilder.inheritIOwerden die Quelle und das Ziel für die Standardprozess-E / A so festgelegt, dass sie mit denen des aktuellen Java-Prozesses übereinstimmen.

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

Wenn Java 7 keine Option ist

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 sterben automatisch ab, wenn der Unterprozess abgeschlossen ist, da srcEOF.


1
Ich sehe, dass Java 7 eine Reihe interessanter Methoden für den Umgang mit stdout, stderr und stdin hinzugefügt hat. Sehr schön. Ich denke, ich werde das nächste Mal inheritIO()oder eine dieser praktischen redirect*(ProcessBuilder.Redirect)Methoden verwenden, wenn ich das für ein Java 7-Projekt tun muss. Leider ist mein Projekt Java 6.
LordOfThePigs

Ah, OK hat meine 1.6 Version hinzugefügt
Evgeniy Dorofeev

scmuss geschlossen werden?
Hotohoto


Beachten Sie, dass es auf den OS-Dateiskriptor der übergeordneten JVM und nicht auf die System.out-Streams festgelegt wird. Dies ist daher in Ordnung, um in die Konsolen- oder Shell-Umleitung des übergeordneten Elements zu schreiben, funktioniert jedoch nicht für die Protokollierung von Streams. Diese benötigen noch einen Pump-Thread (Sie können jedoch mindestens stderr zu stdin umleiten, sodass Sie nur einen Thread benötigen.
eckes

20

Eine flexible Lösung mit Java 8 Lambda, mit der Sie eine bereitstellen können Consumer, die die Ausgabe zeilenweise verarbeitet (z. B. protokolliert). run()ist ein Einzeiler ohne aktivierte Ausnahmen. Alternativ zur Implementierung Runnablekann es erweitert Threadwerden, wie andere Antworten vermuten lassen.

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

Sie können es dann zum Beispiel so verwenden:

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

Hier wird der Ausgabestream umgeleitet System.outund der Fehlerstrom auf der Fehlerebene von der protokolliert logger.


Könnten Sie bitte erläutern, wie Sie dies verwenden würden?
Chris Turner

@Robert Die Threads werden automatisch gestoppt, wenn der entsprechende Eingabe- / Fehlerstrom geschlossen wird. Das forEach()in der run()Methode wird blockiert, bis der Stream geöffnet ist und auf die nächste Zeile wartet. Es wird beendet, wenn der Stream geschlossen wird.
Adam Michalik

13

Es ist so einfach wie folgt:

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

Mit .redirectErrorStream (true) weisen Sie den Prozess an, Fehler und Ausgabestream zusammenzuführen, und mit .redirectOutput (Datei) leiten Sie die zusammengeführte Ausgabe in eine Datei um.

Aktualisieren:

Ich habe das wie folgt geschafft:

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
}

Jetzt können Sie beide Ausgaben von Haupt- und AsyncOut-Threads in System.out sehen


Dies beantwortet nicht die Frage: Ich möchte erfassen, was stdout und / oder stderr dieses Prozesses durchläuft, und es asynchron zu System.out umleiten. Ich möchte, dass der Prozess und seine Ausgabeumleitung im Hintergrund ausgeführt werden.
Adam Michalik

@AdamMichalik, du hast recht - ich habe das Wesentliche zuerst nicht verstanden. Danke fürs zeigen.
Nike.laos

Dies hat das gleiche Problem wie inheritIO (), es schreibt in die poarenten JVMs FD1, ersetzt jedoch keine System.out OutputStreams (wie den Logger-Adapter).
eckes

3

Einfache Java8-Lösung mit Erfassung beider Ausgaben und reaktiver Verarbeitung mit 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);
        }
    });
}

Und die Verwendung:

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

Diese Lösung ist sehr einfach. Es zeigt auch indirekt, wie man dh zu einem Logger umleitet. Schauen Sie sich zum Beispiel meine Antwort an.
Keocra

3

Es gibt eine Bibliothek, die einen besseren ProcessBuilder bietet, zt-exec. Diese Bibliothek kann genau das tun, wonach Sie fragen und mehr.

So würde Ihr Code mit zt-exec anstelle von ProcessBuilder aussehen:

Fügen Sie die Abhängigkeit hinzu:

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

Der Code :

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

Die Dokumentation der Bibliothek finden Sie hier: https://github.com/zeroturnaround/zt-exec/


2

Auch ich kann nur Java 6 verwenden. Ich habe die Thread-Scanner-Implementierung von @ EvgeniyDorofeev verwendet. In meinem Code muss ich nach Abschluss eines Prozesses sofort zwei weitere Prozesse ausführen, die jeweils die umgeleitete Ausgabe vergleichen (ein diff-basierter Komponententest, um sicherzustellen, dass stdout und stderr mit den gesegneten identisch sind).

Die Scanner-Threads werden nicht früh genug beendet, selbst wenn ich darauf warte, dass () der Vorgang abgeschlossen ist. Damit der Code korrekt funktioniert, muss ich sicherstellen, dass die Threads nach Abschluss des Prozesses verbunden werden.

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

Als Ergänzung zur Antwort von msangel möchte ich den folgenden Codeblock hinzufügen:

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

Es ermöglicht die Umleitung des Eingabestreams (stdout, stderr) des Prozesses an einen anderen Verbraucher. Dies kann System.out :: println oder etwas anderes sein, das Zeichenfolgen verbraucht.

Verwendung:

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

Ihr benutzerdefinierter Code wird anstelle von ...


-2

Standardmäßig verfügt der erstellte Unterprozess nicht über ein eigenes Terminal oder eine eigene Konsole. Alle Standard-E / A-Operationen (dh stdin, stdout, stderr) werden an den übergeordneten Prozess umgeleitet, wo auf sie über die Streams zugegriffen werden kann, die mit den Methoden getOutputStream (), getInputStream () und getErrorStream () abgerufen wurden. Der übergeordnete Prozess verwendet diese Streams, um Eingaben in den Unterprozess einzuspeisen und von diesen abzurufen. Da einige native Plattformen nur eine begrenzte Puffergröße für Standardeingabe- und -ausgabestreams bereitstellen, kann das Versäumnis, den Eingabestream sofort zu schreiben oder den Ausgabestream des Unterprozesses zu lesen, dazu führen, dass der Unterprozess blockiert oder sogar blockiert.

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.