ProcessBuilder:转发已启动进程的stdout和stderr,而不会阻塞主线程


93

我正在使用ProcessBuilder在Java中构建一个流程,如下所示:

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

InputStream stdOut = p.getInputStream();

现在我的问题如下:我想捕获通过该进程的stdout和/或stderr进行的操作,并将其System.out异步重定向到。我希望该过程及其输出重定向在后台运行。到目前为止,我发现做到这一点的唯一方法是手动产生一个新线程,该线程将不断读取stdOut并调用适当的write()方法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();

虽然这种方法行得通,但感觉有点脏。最重要的是,它为我提供了另外一个线程来正确管理和终止。有什么更好的方法吗?


2
如果阻止调用线程是一种选择,那么即使在Java 6中也将有一个非常简单的解决方案:org.apache.commons.io.IOUtils.copy(new ProcessBuilder().command(commandLine) .redirectErrorStream(true).start().getInputStream(), System.out);
13年

Answers:


69

Java 6或更早版本中,唯一的办法是使用所谓的StreamGobbler(开始创建):

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

对于Java 7,请参见Evgeniy Dorofeev的答案。


1
StreamGobbler会捕获所有输出吗?是否有可能丢失某些输出?一旦进程停止,StreamGobbler线程也会自行死亡吗?
LordOfThePigs

1
从InputStream结束的那一刻起,它也将结束。
13年

7
对于Java 6及更早版本,这似乎是唯一的解决方案。对于Java 7及更高版本,请参见有关ProcessBuilder.inheritIO()的其他答案
LordOfThePigs 2013年

@asgoth有什么方法可以将输入发送到流程吗?这是我的问题:stackoverflow.com/questions/28070841/…,如果有人能帮助我解决问题,我将不胜感激。
DeepSidhu1313

144

使用ProcessBuilder.inheritIO,它将子流程标准I / O的源和目的地设置为与当前Java流程相同。

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

如果不是Java 7

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

子进程完成时,线程将自动死亡,因为srcEOF 会自动死亡。


1
我看到Java 7添加了许多有趣的方法来处理stdout,stderr和stdin。挺棒的。我想下一次需要在Java 7项目上使用时,我将使用inheritIO()一种或便捷的redirect*(ProcessBuilder.Redirect)方法。不幸的是我的项目是Java的6
LordOfThePigs

嗯,确定添加了我的1.6版本
Evgeniy Dorofeev

sc需要关闭吗?
hotohoto


请注意,它将其设置为父JVM的OS文件描述符,而不是System.out流。因此,可以写入父级的控制台或Shell重定向,但不能用于记录流。那些仍然需要泵线程(但是您至少可以将stderr重定向到stdin,因此您只需要一个线程。)
eckes

20

带有Java 8 lambda的灵活解决方案,可让您提供一个Consumer逐行处理输出(例如记录它)的工具。run()是一个单行,没有引发任何检查异常。Runnable除了实现之外,它还可以Thread根据其他答案的建议进行扩展。

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

然后,您可以像这样使用它:

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

在这里,输出流被重定向到System.out,错误流由记录在错误级别logger


您能否扩大使用方式?
克里斯·特纳

@Robert当相应的输入/错误流关闭时,线程将自动停止。的forEach()run()方法将阻塞,直到该流是开放的,等待下一行。流关闭时将退出。
Adam Michalik

13

它很简单,如下所示:

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

通过.redirectErrorStream(true),您告诉进程合并错误和输出流,然后通过.redirectOutput(file),您将合并的输出重定向到文件。

更新:

我确实做到了如下:

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
}

现在您可以在System.out中看到来自主线程和asyncOut线程的输出


这不能回答问题:我想捕获通过该进程的stdout和/或stderr进行的操作,并将其异步重定向到System.out。我希望该过程及其输出重定向在后台运行。
亚当·米恰里克

@AdamMichalik,您是对的-起初我没有明白要点。感谢您的展示。
nike.laos

这具有与heriterIO()相同的问题,它将写入性能欠佳的JVM FD1,但不会写入任何替换System.out OutputStreams(如记录程序适配器)。
eckes

3

简单的java8解决方案,使用以下命令捕获输出和响应处理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);
        }
    });
}

用法:

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

这个解决方案非常简单。它还间接显示了如何重定向,即重定向到记录器。例如看一下我的答案。
keocra

3

有一个提供更好的ProcessBuilder zt-exec的库。该库可以完全满足您的要求,甚至更多。

这是使用zt-exec而不是ProcessBuilder的代码的样子:

添加依赖项:

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

代码 :

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

该库的文档在这里:https : //github.com/zeroturnaround/zt-exec/


2

我也只能使用Java6。我使用了@EvgeniyDorofeev的线程扫描器实现。在我的代码中,一个进程完成后,我必须立即执行另外两个进程,每个进程都会比较重定向的输出(基于差异的单元测试,以确保stdout和stderr与被祝福的相同)。

扫描程序线程的完成时间不够快,即使我等待For()过程完成。为了使代码正常工作,我必须确保在过程完成之后加入线程。

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

作为msangel 答案的补充,我想添加以下代码块:

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

它允许将流程的输入流(stdout,stderr)重定向到其他使用者。这可能是System.out :: println或其他消耗字符串的东西。

用法:

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

您的自定义代码取代了 ...


-2

默认情况下,创建的子进程没有自己的终端或控制台。其所有标准I / O(即stdin,stdout,stderr)操作都将重定向到父进程,在该父进程中,可以通过使用getOutputStream(),getInputStream()和getErrorStream()方法获得的流来访问它们。父流程使用这些流将输入馈入子流程并从子流程获取输出。因为某些本机平台仅为标准输入和输出流提供了有限的缓冲区大小,所以未能及时写入子流程的输入流或读取子流程的输出流可能导致子流程阻塞甚至死锁。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.