web-dev-qa-db-fra.com

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

Je construis un processus dans Java en utilisant 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 vers System.out De maniè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é de faire cela est de générer manuellement un nouveau thread qui lira continuellement à partir de stdOut, puis appellera la méthode write() 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?

83
LordOfThePigs

La seule façon d'entrer dans Java 6 ou une version antérieure est avec ce qu'on appelle StreamGobbler (que vous avez commencé à 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 de Evgeniy Dorofeev.

65
asgoth

Utilisation ProcessBuilder.inheritIO, la source et la destination des E/S standard de sous-processus sont identiques à celles du processus en cours Java.

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 src aura EOF.

131
Evgeniy Dorofeev

Une solution flexible avec Java 8 lambda qui vous permet de fournir un Consumer qui traitera la sortie (par exemple, enregistrez-le) ligne par ligne. run() est un one-liner sans exception vérifiée levée. Sinon, il est possible d’étendre Runnable comme prolongement de Thread 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.out et le flux d’erreurs est consigné au niveau erreur par le logger.

17
Adam Michalik

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 que le processus fusion de l'erreur et du flux de sortie, puis par .redirectOutput (fichier), vous redirigez la sortie fusionnée vers un fichier.

Mise à jour:

J'ai réussi à faire ceci 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 principaux et asyncOut dans System.out

10
nike.laos

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

Les threads du scanner ne finissent pas assez tôt, même si j'attends que le processus se termine. 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();
        }
    });
}
2
Jeff Holt
Thread thread = new Thread(() -> {
      new BufferedReader(
          new InputStreamReader(inputStream, 
                                StandardCharsets.UTF_8))
              .lines().forEach(...);
    });
    thread.start();

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

0
Maxim

Solution Java8 simple avec capture des sorties et traitement réactif à l'aide de 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'usage:

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