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

时间:2013-01-04 21:46:09

标签: java processbuilder

我正在使用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()的{​​{1}}方法。

System.out

虽然这种方法很有效,但感觉有点脏。最重要的是,它为我提供了一个正确管理和终止的线程。有没有更好的方法呢?

12 个答案:

答案 0 :(得分:127)

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

子进程完成后,线程将自动死亡,因为src将为EOF。

答案 1 :(得分:64)

仅在 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的回答。

答案 2 :(得分:17)

使用Java 8 lambda的灵活解决方案,允许您提供将逐行处理输出(例如,记录)的Consumerrun()是一个单行程序,没有检查异常。作为实施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记录在错误级别。

答案 3 :(得分:10)

这很简单:

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

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

<强>更新

我确实按照以下方式执行此操作:

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

中看到main和asyncOut线程的输出

答案 4 :(得分: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/

答案 5 :(得分:2)

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

即使我在waitFor()过程中完成,扫描程序线程也不会很快完成。为了使代码正常工作,我必须确保在进程完成后连接线程。

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

答案 6 :(得分:1)

使用CompletableFuture捕获输出和响应处理的简单java8解决方案:

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

答案 7 :(得分:1)

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

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

答案 8 :(得分:0)

Thread thread = new Thread(() -> {
      new BufferedReader(
          new InputStreamReader(inputStream, 
                                StandardCharsets.UTF_8))
              .lines().forEach(...);
    });
    thread.start();

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

答案 9 :(得分:0)

令我惊讶的是 ProcessBuilder 中的重定向方法不接受 OutputStream,只接受 File。 Java 强迫您编写的强制样板代码的另一个证明。

也就是说,让我们看一下综合选项列表:

  1. 如果您希望将流程输出简单地重定向到其父级的输出流,inheritIO 将完成这项工作。
  2. 如果您希望流程输出转到文件,请使用 redirect*(file)
  3. 如果您希望进程输出到记录器,则需要在单独的线程中使用进程 InputStream。查看使用 RunnableCompletableFuture 的答案。您也可以修改下面的代码来执行此操作。
  4. 如果您想让流程输出转到 PrintWriter,它可能是也可能不是标准输出(对于测试非常有用),您可以执行以下操作:
static int execute(List<String> args, PrintWriter out) {
    ProcessBuilder builder = new ProcessBuilder()
            .command(args)
            .redirectErrorStream(true);
    Process process = null;
    boolean complete = false;
    try {
        process = builder.start();
        redirectOut(process.getInputStream(), out)
                .orTimeout(TIMEOUT, TimeUnit.SECONDS);
        complete = process.waitFor(TIMEOUT, TimeUnit.SECONDS);
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    } catch (InterruptedException e) {
        LOG.warn("Thread was interrupted", e);
    } finally {
        if (process != null && !complete) {
            LOG.warn("Process {} didn't finish within {} seconds", args.get(0), TIMEOUT);
            process = process.destroyForcibly();
        }
    }

    return process != null ? process.exitValue() : 1;
}

private static CompletableFuture<Void> redirectOut(InputStream in, PrintWriter out) {
    return CompletableFuture.runAsync(() -> {
        try (
                InputStreamReader inputStreamReader = new InputStreamReader(in);
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader)
        ) {
            bufferedReader.lines()
                    .forEach(out::println);
        } catch (IOException e) {
            LOG.error("Failed to redirect process output", e);
        }
    });
}

以上代码相对于其他答案的优势:

  1. redirectErrorStream(true) 将错误流重定向到输出流,这样我们只需要处理一个。
  2. CompletableFuture.runAsyncForkJoinPool 运行。请注意,此代码不会通过在 get 上调用 joinCompletableFuture 来阻止,而是在其完成时设置超时(Java 9+)。不需要 CompletableFuture.supplyAsync,因为方法 redirectOut 没有什么可真正返回。
  3. BufferedReader.lines 比使用 while 循环更简单。

答案 10 :(得分:-1)

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Main {

    public static void main(String[] args) throws Exception {
        ProcessBuilder pb = new ProcessBuilder("script.bat");
        pb.redirectErrorStream(true);
        Process p = pb.start();
        BufferedReader logReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
        String logLine = null;
        while ( (logLine = logReader.readLine()) != null) {
           System.out.println("Script output: " + logLine);
        }
    }
}

使用以下行:pb.redirectErrorStream(true);,我们可以将InputStream和ErrorStream组合起来

答案 11 :(得分:-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