简体   繁体   English

ProcessBuilder:在不阻塞主线程的情况下转发已启动进程的 stdout 和 stderr

[英]ProcessBuilder: Forwarding stdout and stderr of started processes without blocking the main thread

I'm building a process in Java using ProcessBuilder as follows:我正在使用 ProcessBuilder 在 Java 中构建一个进程,如下所示:

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

InputStream stdOut = p.getInputStream();

Now my problem is the following: I would like to capture whatever is going through stdout and/or stderr of that process and redirect it to System.out asynchronously.现在我的问题如下:我想捕获通过该进程的 stdout 和/或 stderr 的任何内容,并将其异步重定向到System.out I want the process and its output redirection to run in the background.我希望进程及其输出重定向在后台运行。 So far, the only way I've found to do this is to manually spawn a new thread that will continuously read from stdOut and then call the appropriate write() method of System.out .到目前为止,我发现这样做的唯一方法是手动生成一个新线程,该线程将连续从stdOut读取,然后调用System.out的适当write()方法。

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

While that approach kind of works, it feels a bit dirty.虽然这种方法有效,但感觉有点脏。 And on top of that, it gives me one more thread to manage and terminate correctly.最重要的是,它给了我一个线程来正确管理和终止。 Is there any better way to do this?有没有更好的方法来做到这一点?

Use ProcessBuilder.inheritIO , it sets the source and destination for subprocess standard I/O to be the same as those of the current Java process.使用ProcessBuilder.inheritIO ,它将子进程标准 I/O 的源和目标设置为与当前 Java 进程相同。

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

If Java 7 is not an option如果 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();
}

Threads will die automatically when subprocess finishes, because src will EOF.当子进程完成时,线程将自动死亡,因为src将 EOF。

For Java 7 and later , see Evgeniy Dorofeev's answer .对于Java 7 及更高版本,请参阅 Evgeniy Dorofeev 的回答

For Java 6 and earlier , create and use a StreamGobbler :对于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();
        }
    }
}

A flexible solution with Java 8 lambda that lets you provide a Consumer that will process the output (eg. log it) line by line.使用 Java 8 lambda 的灵活解决方案,可让您提供一个Consumer来逐行处理输出(例如记录它)。 run() is a one-liner with no checked exceptions thrown. run()是单行的,没有抛出已检查的异常。 Alternatively to implementing Runnable , it can extend Thread instead as other answers suggest.替代实现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);
    }
}

You can then use it for example like this:然后你可以像这样使用它:

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

Here the output stream is redirected to System.out and the error stream is logged on the error level by the logger .这里输出流被重定向到System.out并且错误流被记录logger记录在错误级别。

It's as simple as following:这很简单,如下所示:

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

by .redirectErrorStream(true) you tell process to merge error and output stream and then by .redirectOutput(file) you redirect merged output to a file.通过 .redirectErrorStream(true) 告诉进程合并错误和输出流,然后通过 .redirectOutput(file) 将合并的输出重定向到文件。

Update:更新:

I did manage to do this as follows:我确实设法做到这一点如下:

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
}

Now you're able to see both outputs from main and asyncOut threads in System.out现在您可以在System.out 中看到 main 和 asyncOut 线程的输出

Simple java8 solution with capturing both outputs and reactive processing using CompletableFuture :使用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);
        }
    });
}

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

There is a library that provides a better ProcessBuilder, zt-exec.有一个库提供了更好的 ProcessBuilder,zt-exec。 This library can do exactly what you are asking for and more.这个库可以完全满足您的要求,甚至更多。

Here's what your code would look like with zt-exec instead of ProcessBuilder :以下是使用 zt-exec 而不是 ProcessBuilder 的代码:

add the dependency :添加依赖项:

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

The code :代码 :

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

Documentation of the library is here : https://github.com/zeroturnaround/zt-exec/该库的文档在这里: https : //github.com/zeroturnaround/zt-exec/

I too can use only Java 6. I used @EvgeniyDorofeev's thread scanner implementation.我也只能使用 Java 6。我使用了 @EvgeniyDorofeev 的线程扫描器实现。 In my code, after a process finishes, I have to immediately execute two other processes that each compare the redirected output (a diff-based unit test to ensure stdout and stderr are the same as the blessed ones).在我的代码中,在一个进程完成后,我必须立即执行另外两个进程,每个进程都比较重定向的输出(基于差异的单元测试,以确保 stdout 和 stderr 与祝福的相同)。

The scanner threads don't finish soon enough, even if I waitFor() the process to complete.即使我 waitFor() 进程完成,扫描器线程也不会很快完成。 To make the code work correctly, I have to make sure the threads are joined after the process finishes.为了使代码正常工作,我必须确保在进程完成后加入线程。

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

As an addition to msangel answer I would like to add the following code block:作为 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;
            }
        });
    }

It allows to redirect the input stream (stdout, stderr) of the process to some other consumer.它允许将进程的输入流(stdout、stderr)重定向到其他一些消费者。 This might be System.out::println or anything else consuming strings.这可能是 System.out::println 或任何其他消耗字符串的东西。

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

Your custom code goes instead of the ...您的自定义代码代替...

It's really surprising to me that the redirection methods in ProcessBuilder don't accept an OutputStream , only File .令我惊讶的是ProcessBuilder中的重定向方法不接受OutputStream ,只接受File Yet another proof of forced boilerplate code that Java forces you to write. Java 强迫您编写的强制样板代码的另一个证明。

That said, let's look at a list of comprehensive options:也就是说,让我们看一下综合选项列表:

  1. If you want the process output to simply be redirected to its parent's output stream, inheritIO will do the job.如果您希望将流程输出简单地重定向到其父级的输出流, inheritIO将完成这项工作。
  2. If you want the process output to go to a file, use redirect*(file) .如果您希望进程输出转到文件,请使用redirect*(file)
  3. If you want the process output to go to a logger, you need to consume the process InputStream in a separate thread.如果您希望进程输出到记录器,则需要在单独的线程中使用进程InputStream See the answers that use a Runnable or CompletableFuture .查看使用RunnableCompletableFuture的答案。 You can also adapt the code below to do this.您还可以修改下面的代码来执行此操作。
  4. If you want to the process output to go to a PrintWriter , that may or may not be the stdout (very useful for testing), you can do the following:如果您希望处理输出转到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);
        }
    });
}

Advantages of the code above over the other answers thus far:到目前为止,上述代码相对于其他答案的优势:

  1. redirectErrorStream(true) redirects the error stream to the output stream, so that we only have to bother with one. redirectErrorStream(true)将错误流重定向到输出流,这样我们只需要考虑一个。
  2. CompletableFuture.runAsync runs from the ForkJoinPool . CompletableFuture.runAsyncForkJoinPool运行。 Note that this code doesn't block by calling get or join on the CompletableFuture but sets a timeout instead on its completion (Java 9+).请注意,此代码不会通过在CompletableFuture上调用getjoin来阻止,而是在其完成时设置超时(Java 9+)。 There's no need for CompletableFuture.supplyAsync because there's nothing really to return from the method redirectOut .不需要CompletableFuture.supplyAsync因为从方法redirectOut没有什么可返回的。
  3. BufferedReader.lines is simpler than using a while loop. BufferedReader.lines比使用while循环更简单。
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);
        }
    }
}

By using this line: pb.redirectErrorStream(true);通过使用这一行: pb.redirectErrorStream(true); we can combine InputStream and ErrorStream我们可以结合 InputStream 和 ErrorStream

By default, the created subprocess does not have its own terminal or console.默认情况下,创建的子进程没有自己的终端或控制台。 All its standard I/O (ie stdin, stdout, stderr) operations will be redirected to the parent process, where they can be accessed via the streams obtained using the methods getOutputStream(), getInputStream(), and getErrorStream().它的所有标准 I/O(即 stdin、stdout、stderr)操作都将被重定向到父进程,在那里可以通过使用 getOutputStream()、getInputStream() 和 getErrorStream() 方法获得的流访问它们。 The parent process uses these streams to feed input to and get output from the subprocess.父进程使用这些流向子进程提供输入和从子进程获取输出。 Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, or even deadlock.由于一些原生平台只为标准输入输出流提供有限的缓冲区大小,如果不能及时写入输入流或读取子进程的输出流,可能会导致子进程阻塞,甚至死锁。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM