ProcessBuilder: การส่งต่อ stdout และ stderr ของกระบวนการที่เริ่มต้นโดยไม่บล็อกเธรดหลัก


94

ฉันกำลังสร้างกระบวนการใน Java โดยใช้ ProcessBuilder ดังนี้:

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

แม้ว่าวิธีการทำงานแบบนั้นจะรู้สึกสกปรกเล็กน้อย และยิ่งไปกว่านั้นมันทำให้ฉันมีอีกหนึ่งเธรดในการจัดการและยุติอย่างถูกต้อง มีวิธีไหนที่ดีกว่านี้ไหม?


3
หากการบล็อกเธรดการโทรเป็นตัวเลือกจะมีวิธีแก้ปัญหาที่ง่ายมากแม้ใน Java 6:org.apache.commons.io.IOUtils.copy(new ProcessBuilder().command(commandLine) .redirectErrorStream(true).start().getInputStream(), System.out);
ปฏิบัติตาม

คำตอบ:


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 สิ้นสุดลงมันก็จะสิ้นสุดลงเช่นกัน
asgoth

7
สำหรับ Java 6 และรุ่นก่อนหน้าดูเหมือนว่านี่จะเป็นทางออกเดียว สำหรับ java 7 ขึ้นไปดูคำตอบอื่น ๆ เกี่ยวกับ ProcessBuilder.inheritIO ()
LordOfThePigs

@asgoth มีวิธีใดในการส่งข้อมูลเข้าสู่กระบวนการหรือไม่? นี่คือคำถามของฉัน: stackoverflow.com/questions/28070841/…ฉันจะขอบคุณถ้ามีคนช่วยฉันแก้ปัญหา
DeepSidhu1313

145

ใช้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
ฉันเห็นว่า Java 7 ได้เพิ่มวิธีการที่น่าสนใจมากมายสำหรับจัดการ stdout, stderr และ stdin สวยดี ฉันคิดว่าฉันจะใช้inheritIO()หรือหนึ่งในredirect*(ProcessBuilder.Redirect)วิธีที่มีประโยชน์เหล่านั้นในครั้งต่อไปที่ฉันต้องทำในโครงการ java 7 น่าเสียดายที่โครงการของฉันคือ java 6
LordOfThePigs

อาตกลงเพิ่มรุ่น 1.6 ของฉัน
Evgeniy Dorofeev

scต้องปิด?
hotohoto

คุณช่วยเรื่องstackoverflow.com/questions/43051640/…ได้ไหม
gstackoverflow

โปรดสังเกตว่ามันตั้งค่าเป็นไฟล์ OS ตัวอธิบายของ JVM พาเรนต์ไม่ใช่สตรีม System.out ดังนั้นจึงเป็นเรื่องปกติที่จะเขียนไปยังคอนโซลหรือการเปลี่ยนเส้นทางเชลล์ของพาเรนต์ แต่จะใช้ไม่ได้กับการบันทึกสตรีม สิ่งเหล่านี้ยังคงต้องการด้ายปั๊ม (แต่อย่างน้อยคุณสามารถเปลี่ยนเส้นทาง stderr ไปที่ stdin ได้ดังนั้นคุณต้องใช้ด้ายเพียงเส้นเดียว
eckes

20

โซลูชันที่ยืดหยุ่นด้วย Java 8 lambda ที่ให้คุณจัดเตรียมConsumerที่จะประมวลผลเอาต์พุต (เช่น log it) ทีละบรรทัด 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.


คุณช่วยขยายความเกี่ยวกับวิธีการใช้งานนี้ได้ไหม
Chris Turner

@Robert เธรดจะหยุดโดยอัตโนมัติเมื่อปิดสตรีมอินพุต / ข้อผิดพลาดที่เกี่ยวข้อง forEach()ในrun()วิธีการที่จะป้องกันจนกระทั่งกระแสเปิดรอบรรทัดถัดไป จะออกเมื่อสตรีมถูกปิด
Adam Michalik

16

ง่ายๆดังนี้:

    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
}

ตอนนี้คุณสามารถเห็นทั้งเอาต์พุตจากเธรดหลักและ asyncOut ในSystem.out


สิ่งนี้ไม่ตอบคำถาม: ฉันต้องการจับภาพสิ่งที่กำลังดำเนินอยู่ผ่าน stdout และ / หรือ stderr ของกระบวนการนั้นและเปลี่ยนเส้นทางไปที่ System.out แบบอะซิงโครนัส ฉันต้องการให้กระบวนการและการเปลี่ยนเส้นทางเอาต์พุตทำงานในพื้นหลัง
Adam Michalik

@AdamMichalik คุณพูดถูก - ตอนแรกฉันไม่เข้าใจ ขอบคุณสำหรับการแสดง
nike.laos

สิ่งนี้มีปัญหาเช่นเดียวกับ inheritIO () ซึ่งจะเขียนไปยัง JVMs FD1 ที่ชัดเจน แต่จะไม่แทนที่ System.out OutputStreams ใด ๆ
eckes

4

โซลูชัน 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

ฉันสามารถใช้เฉพาะ Java 6 ได้เช่นกันฉันใช้การติดตั้งเครื่องสแกนเธรดของ @ EvgeniyDorofeev ในรหัสของฉันหลังจากกระบวนการเสร็จสิ้นฉันต้องดำเนินการทันทีอีกสองกระบวนการที่แต่ละกระบวนการเปรียบเทียบผลลัพธ์ที่เปลี่ยนเส้นทาง (การทดสอบหน่วยที่ใช้ diff-based เพื่อให้แน่ใจว่า stdout และ stderr เหมือนกับกระบวนการที่ได้รับพร)

เธรดของสแกนเนอร์ยังไม่เสร็จสิ้นเร็วพอแม้ว่าฉันจะรอให้ () กระบวนการเสร็จสมบูรณ์ เพื่อให้โค้ดทำงานได้อย่างถูกต้องฉันต้องตรวจสอบให้แน่ใจว่าเธรดถูกรวมเข้าด้วยกันหลังจากกระบวนการเสร็จสิ้น

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

รหัสที่กำหนดเองของคุณไปแทน ...


-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


ซึ่งใช้ไปแล้วในโค้ดที่อยู่ในคำถาม ...
LordOfThePigs

-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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.