ProcessBuilder: Chuyển tiếp stdout và stderr của các quy trình đã bắt đầu mà không chặn luồng chính


93

Tôi đang xây dựng một quy trình trong Java bằng ProcessBuilder như sau:

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

InputStream stdOut = p.getInputStream();

Bây giờ vấn đề của tôi là như sau: Tôi muốn nắm bắt bất cứ điều gì đang diễn ra qua stdout và / hoặc stderr của quá trình đó và chuyển hướng nó đến System.outkhông đồng bộ. Tôi muốn quá trình và chuyển hướng đầu ra của nó chạy trong nền. Cho đến nay, cách duy nhất tôi tìm thấy để làm điều này là tạo ra một chuỗi mới theo cách thủ công sẽ liên tục đọc từ stdOutđó và sau đó gọi write()phương thức thích hợp của 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();

Trong khi cách tiếp cận đó hoạt động, nó có cảm giác hơi bẩn. Và trên hết, nó cung cấp cho tôi thêm một chuỗi để quản lý và kết thúc một cách chính xác. Có cách nào tốt hơn để làm điều này không?


2
Nếu chặn thread gọi là một lựa chọn, đó sẽ là một giải pháp rất đơn giản, ngay cả trong Java 6:org.apache.commons.io.IOUtils.copy(new ProcessBuilder().command(commandLine) .redirectErrorStream(true).start().getInputStream(), System.out);
oberlies

Câu trả lời:


69

Cách duy nhất trong Java 6 hoặc phiên bản cũ hơn là với cái gọi là StreamGobbler(mà bạn đã bắt đầu tạo):

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

Đối với Java 7, hãy xem câu trả lời của Evgeniy Dorofeev.


1
StreamGobbler có bắt được tất cả đầu ra không? Có bất kỳ cơ hội nào để nó thiếu một số đầu ra không? Ngoài ra, chuỗi StreamGobbler có tự chết sau khi quá trình dừng lại không?
LordOfThePigs

1
Từ thời điểm này, InputStream đã kết thúc, nó cũng sẽ kết thúc.
asgoth

7
Đối với Java 6 trở về trước, có vẻ như đây là giải pháp duy nhất. Đối với java 7 trở lên, xem câu trả lời khác về ProcessBuilder.inheritIO ()
LordOfThePigs

@asgoth Có cách nào để gửi đầu vào cho quy trình không? Đây là câu hỏi của tôi: stackoverflow.com/questions/28070841/… , tôi sẽ biết ơn nếu ai đó sẽ giúp tôi giải quyết vấn đề.
DeepSidhu1313

144

Sử dụng ProcessBuilder.inheritIO, nó đặt nguồn và đích cho I / O tiêu chuẩn quy trình con giống với quy trình Java hiện tại.

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

Nếu Java 7 không phải là một tùy chọn

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

Các luồng sẽ tự động chết khi quá trình con kết thúc, vì srcsẽ EOF.


1
Tôi thấy rằng Java 7 đã thêm một loạt các phương thức thú vị để xử lý stdout, stderr và stdin. Khá đẹp. Tôi nghĩ tôi sẽ sử dụng inheritIO()hoặc một trong những redirect*(ProcessBuilder.Redirect)phương pháp hữu ích đó vào lần tới khi tôi cần làm điều đó trên một dự án java 7. Đáng tiếc là dự án của tôi là java 6.
LordOfThePigs

Ah, OK thêm phiên bản 1.6 của tôi
Evgeniy Dorofeev

sccần phải đóng cửa?
hotohoto

bạn có thể trợ giúp với stackoverflow.com/questions/43051640/… ?
gstackoverflow

Lưu ý rằng nó đặt nó thành bộ ký mã hệ điều hành của JVM mẹ, không phải cho các luồng System.out. Do đó, điều này có thể chấp nhận được để ghi vào bảng điều khiển hoặc chuyển hướng shell của cha mẹ nhưng nó sẽ không hoạt động để ghi nhật ký luồng. Những người đó vẫn cần một luồng bơm (tuy nhiên ít nhất bạn có thể chuyển hướng stderr đến stdin, vì vậy bạn chỉ cần một luồng.
eckes

20

Một giải pháp linh hoạt với Java 8 lambda cho phép bạn cung cấp một giải pháp Consumersẽ xử lý đầu ra (ví dụ: ghi nhật ký) từng dòng một. run()là một lớp lót không có ngoại lệ được kiểm tra nào được ném ra. Ngoài việc triển khai Runnable, nó có thể mở rộng Threadthay thế như các câu trả lời khác đề xuất.

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

Sau đó, bạn có thể sử dụng nó ví dụ như sau:

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

Tại đây luồng đầu ra được chuyển hướng đến System.outvà luồng lỗi được ghi lại mức lỗi bằng logger.


Bạn có thể vui lòng mở rộng cách bạn sử dụng cái này không?
Chris Turner

@Robert Các luồng sẽ tự động dừng khi luồng đầu vào / lỗi tương ứng bị đóng. Các forEach()trong run()phương pháp sẽ chặn cho đến khi dòng mở cửa, chờ dòng tiếp theo. Nó sẽ thoát ra khi luồng bị đóng.
Adam Michalik

13

Nó đơn giản như sau:

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

bởi .redirectErrorStream (true) bạn yêu cầu quá trình kết hợp lỗi và luồng đầu ra và sau đó bằng .redirectOutput (tệp) bạn chuyển hướng đầu ra đã hợp nhất thành một tệp.

Cập nhật:

Tôi đã quản lý để làm điều này như sau:

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
}

Giờ đây, bạn có thể xem cả kết quả đầu ra từ các chuỗi chính và asyncOut trong System.out


Điều này không trả lời câu hỏi: Tôi muốn nắm bắt bất cứ điều gì đang diễn ra qua stdout và / hoặc stderr của quá trình đó và chuyển hướng nó đến System.out một cách không đồng bộ. Tôi muốn quá trình và chuyển hướng đầu ra của nó chạy trong nền.
Adam Michalik

@AdamMichalik, bạn nói đúng - lúc đầu tôi không hiểu ý chính. Cảm ơn vì đã hiển thị.
nike.laos

Điều này có vấn đề tương tự như inheritIO (), nó sẽ ghi vào các JVM chính FD1 nhưng không phải bất kỳ thay thế System.out OutputStreams (như bộ điều hợp trình ghi nhật ký).
eckes

3

Giải pháp java8 đơn giản với việc thu thập cả đầu ra và xử lý phản ứng bằng cách sử dụng 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);
        }
    });
}

Và cách sử dụng:

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

Giải pháp này là rất đơn giản. Nó cũng cho thấy gián tiếp cách chuyển hướng tức là đến một trình ghi nhật ký. Ví dụ, hãy xem câu trả lời của tôi.
keocra

3

Có một thư viện cung cấp ProcessBuilder tốt hơn, zt-executive. Thư viện này có thể làm chính xác những gì bạn đang yêu cầu và hơn thế nữa.

Đây là mã của bạn trông như thế nào với zt-executive thay vì ProcessBuilder:

thêm phần phụ thuộc:

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

Mật mã :

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

Tài liệu của thư viện tại đây: https://github.com/zeroturnaround/zt-exec/


2

Tôi cũng chỉ có thể sử dụng Java 6. Tôi đã sử dụng triển khai trình quét luồng của @ EvgeniyDorofeev. Trong mã của tôi, sau khi một quá trình kết thúc, tôi phải thực hiện ngay lập tức hai quá trình khác, mỗi quá trình sẽ so sánh kết quả được chuyển hướng (một bài kiểm tra đơn vị dựa trên khác biệt để đảm bảo stdout và stderr giống như những cái được may mắn).

Các luồng máy quét không kết thúc đủ sớm, ngay cả khi tôi đợi () quá trình hoàn tất. Để làm cho mã hoạt động chính xác, tôi phải đảm bảo các luồng được nối sau khi quá trình kết thúc.

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

Để bổ sung cho câu trả lời msangel, tôi muốn thêm khối mã sau:

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

Nó cho phép chuyển hướng luồng đầu vào (stdout, stderr) của quy trình tới một số người tiêu dùng khác. Đây có thể là System.out :: println hoặc bất kỳ thứ gì khác sử dụng chuỗi.

Sử dụng:

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

Mã tùy chỉnh của bạn thay vì ...


-2

Theo mặc định, quy trình con được tạo không có thiết bị đầu cuối hoặc bảng điều khiển riêng. Tất cả các hoạt động I / O tiêu chuẩn của nó (tức là stdin, stdout, stderr) sẽ được chuyển hướng đến quy trình mẹ, nơi chúng có thể được truy cập thông qua các luồng thu được bằng các phương thức getOutputStream (), getInputStream () và getErrorStream (). Tiến trình mẹ sử dụng các luồng này để cấp dữ liệu đầu vào và nhận đầu ra từ quy trình con. Bởi vì một số nền tảng gốc chỉ cung cấp kích thước bộ đệm hạn chế cho các luồng đầu vào và đầu ra tiêu chuẩn, việc không ghi kịp thời luồng đầu vào hoặc đọc luồng đầu ra của quy trình con có thể khiến quy trình con bị chặn hoặc thậm chí là bế tắc.

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.