ProcessBuilder: transfert stdout et stderr des processus démarrés sans bloquer le thread principal

93

Je construis un processus en Java à l'aide de ProcessBuilder comme suit:

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

InputStream stdOut = p.getInputStream();

Maintenant, mon problème est le suivant: je voudrais capturer tout ce qui passe par stdout et / ou stderr de ce processus et le rediriger de System.outmanière asynchrone. Je veux que le processus et sa redirection de sortie s'exécutent en arrière-plan. Jusqu'à présent, le seul moyen que j'ai trouvé pour le faire est de créer manuellement un nouveau thread qui lira en continu stdOutet appellera la write()méthode appropriée 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();

Bien que cette approche fonctionne, elle semble un peu sale. Et en plus de cela, cela me donne un fil de plus à gérer et à terminer correctement. Y a-t-il une meilleure façon de faire cela?

LordOfThePigs
la source
2
Si bloquer le thread appelant était une option, il y aurait une solution très simple même en Java 6:org.apache.commons.io.IOUtils.copy(new ProcessBuilder().command(commandLine) .redirectErrorStream(true).start().getInputStream(), System.out);
oberlies

Réponses:

69

Le seul moyen dans Java 6 ou antérieur est avec un soi-disant StreamGobbler(que vous commencez à créer):

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

Pour Java 7, voir la réponse d'Evgeniy Dorofeev.

asgoth
la source
1
Le StreamGobbler capturera-t-il toute la sortie? Y a-t-il une chance qu'il manque une partie de la sortie? Le thread StreamGobbler mourra-t-il également de lui-même une fois le processus stoppé?
LordOfThePigs
1
À partir du moment où le InputStream est terminé, il se terminera également.
asgoth
7
Pour Java 6 et les versions antérieures, il semble que ce soit la seule solution. Pour java 7 et plus, voir l'autre réponse sur ProcessBuilder.inheritIO ()
LordOfThePigs
@asgoth Existe-t-il un moyen d'envoyer des informations au processus? Voici ma question: stackoverflow.com/questions/28070841/… , je serai reconnaissant si quelqu'un m'aidera à résoudre le problème.
DeepSidhu1313
144

À utiliser ProcessBuilder.inheritIO, il définit la source et la destination des E / S standard du sous-processus comme celles du processus Java actuel.

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

Si Java 7 n'est pas une option

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

Les threads mourront automatiquement à la fin du sous-processus, car srcEOF.

Evgeniy Dorofeev
la source
1
Je vois que Java 7 a ajouté un tas de méthodes intéressantes pour gérer stdout, stderr et stdin. Pas mal. Je pense que je vais utiliser inheritIO()ou l'une de ces redirect*(ProcessBuilder.Redirect)méthodes pratiques la prochaine fois que j'aurai besoin de le faire sur un projet java 7. Malheureusement, mon projet est java 6.
LordOfThePigs
Ah, OK a ajouté ma version 1.6
Evgeniy Dorofeev
scdoit être fermé?
hotohoto
pouvez-vous aider avec stackoverflow.com/questions/43051640/… ?
gstackoverflow
Notez qu'il le définit sur le descripteur de fichier du système d'exploitation de la JVM parent, et non sur les flux System.out. C'est donc correct d'écrire dans la console ou la redirection du shell du parent, mais cela ne fonctionnera pas pour la journalisation des flux. Ceux-ci ont encore besoin d'un filetage de pompe (cependant vous pouvez au moins rediriger stderr vers stdin, vous n'avez donc besoin que d'un filetage.
eckes
20

Une solution flexible avec Java 8 lambda qui vous permet de fournir un Consumerqui traitera la sortie (par exemple, la consigner) ligne par ligne. run()est une ligne unique sans exception vérifiée lancée. Alternativement à la mise en œuvre Runnable, il peut s'étendre à la Threadplace comme le suggèrent d'autres réponses.

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

Vous pouvez ensuite l'utiliser par exemple comme ceci:

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

Ici, le flux de sortie est redirigé vers System.outet le flux d'erreur est enregistré au niveau d'erreur par le logger.

Adam Michalik
la source
Pourriez-vous expliquer comment vous utiliseriez cela?
Chris Turner
@Robert Les threads s'arrêteront automatiquement lorsque le flux d'entrée / d'erreur correspondant est fermé. Le forEach()dans la run()méthode se bloquera jusqu'à ce que le flux soit ouvert, en attendant la ligne suivante. Il se fermera lorsque le flux sera fermé.
Adam Michalik
13

C'est aussi simple que de suivre:

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

par .redirectErrorStream (true) vous indiquez au processus de fusionner l'erreur et le flux de sortie , puis par .redirectOutput (fichier) vous redirigez la sortie fusionnée vers un fichier.

Mettre à jour:

J'ai réussi à le faire comme suit:

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
}

Vous pouvez maintenant voir les deux sorties des threads principal et asyncOut dans System.out

nike.laos
la source
Cela ne répond pas à la question: je voudrais capturer tout ce qui passe par stdout et / ou stderr de ce processus et le rediriger vers System.out de manière asynchrone. Je veux que le processus et sa redirection de sortie s'exécutent en arrière-plan.
Adam Michalik
@AdamMichalik, vous avez raison - je n'ai pas compris l'essentiel, au début. Merci d'avoir montré.
nike.laos
Cela pose le même problème que inheritIO (), il écrira sur les JVM FD1 mais pas sur les autres System.out OutputStreams (comme l'adaptateur de journalisation).
eckes le
3

Solution java8 simple avec capture des sorties et traitement réactif en utilisant 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);
        }
    });
}

Et l'utilisation:

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
la source
Cette solution est très simple. Il montre également indirectement comment rediriger ie vers un enregistreur. Pour un exemple, regardez ma réponse.
keocra
3

Il existe une bibliothèque qui fournit un meilleur ProcessBuilder, zt-exec. Cette bibliothèque peut faire exactement ce que vous demandez et plus encore.

Voici à quoi ressemblerait votre code avec zt-exec au lieu de ProcessBuilder:

ajoutez la dépendance:

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

Le code :

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

La documentation de la bibliothèque est ici: https://github.com/zeroturnaround/zt-exec/

mryan
la source
2

Moi aussi, je ne peux utiliser que Java 6. J'ai utilisé l'implémentation du scanner de threads de @ EvgeniyDorofeev. Dans mon code, une fois le processus terminé, je dois exécuter immédiatement deux autres processus qui comparent chacun la sortie redirigée (un test unitaire basé sur des différences pour s'assurer que stdout et stderr sont les mêmes que les bénis).

Les threads du scanner ne se terminent pas assez tôt, même si j'attends () la fin du processus. Pour que le code fonctionne correctement, je dois m'assurer que les threads sont joints une fois le processus terminé.

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
la source
1

En plus de la réponse msangel, je voudrais ajouter le bloc de code suivant:

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

Il permet de rediriger le flux d'entrée (stdout, stderr) du processus vers un autre consommateur. Cela peut être System.out :: println ou tout autre élément consommant des chaînes.

Usage:

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

Votre code personnalisé va à la place du ...

Maxime
la source
-2

Par défaut, le sous-processus créé n'a pas son propre terminal ou console. Toutes ses opérations d'E / S standard (c'est-à-dire stdin, stdout, stderr) seront redirigées vers le processus parent, où elles seront accessibles via les flux obtenus en utilisant les méthodes getOutputStream (), getInputStream () et getErrorStream (). Le processus parent utilise ces flux pour alimenter en entrée et obtenir la sortie du sous-processus. Étant donné que certaines plates-formes natives ne fournissent qu'une taille de tampon limitée pour les flux d'entrée et de sortie standard, l'échec de l'écriture rapide du flux d'entrée ou de la lecture du flux de sortie du sous-processus peut entraîner le blocage du sous-processus, voire un blocage.

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

Sonal Kumar Sinha
la source