กระบวนการ Java พร้อมอินพุต / เอาต์พุตสตรีม


91

ฉันมีตัวอย่างโค้ดต่อไปนี้ด้านล่าง โดยคุณสามารถป้อนคำสั่งไปที่ bash shell ie echo testและให้ผลลัพธ์สะท้อนกลับ อย่างไรก็ตามหลังจากอ่านครั้งแรก กระแสข้อมูลอื่น ๆ ไม่ทำงาน?

เหตุใดจึงเป็นเช่นนี้หรือฉันทำอะไรผิด? เป้าหมายสุดท้ายของฉันคือการสร้างงานตามกำหนดเวลาเธรดที่ดำเนินการคำสั่งเป็นระยะ ๆ เพื่อ / ทุบตีดังนั้นOutputStreamและInputStreamจะต้องทำงานควบคู่และไม่หยุดทำงาน ฉันยังพบข้อผิดพลาดเกี่ยวjava.io.IOException: Broken pipeกับความคิดใด ๆ ?

ขอบคุณ.

String line;
Scanner scan = new Scanner(System.in);

Process process = Runtime.getRuntime ().exec ("/bin/bash");
OutputStream stdin = process.getOutputStream ();
InputStream stderr = process.getErrorStream ();
InputStream stdout = process.getInputStream ();

BufferedReader reader = new BufferedReader (new InputStreamReader(stdout));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin));

String input = scan.nextLine();
input += "\n";
writer.write(input);
writer.flush();

input = scan.nextLine();
input += "\n";
writer.write(input);
writer.flush();

while ((line = reader.readLine ()) != null) {
System.out.println ("Stdout: " + line);
}

input = scan.nextLine();
input += "\n";
writer.write(input);
writer.close();

while ((line = reader.readLine ()) != null) {
System.out.println ("Stdout: " + line);
}

"ท่อแตก" อาจหมายถึงกระบวนการลูกได้ออกไปแล้ว คุณยังไม่ได้ดูโค้ดส่วนที่เหลือทั้งหมดเพื่อดูว่ามีปัญหาอะไรอีกบ้าง
vanza

1
ใช้เธรดแยกกันมันจะทำงานได้ดี
Johnydep

คำตอบ:


140

ประการแรกฉันอยากจะแนะนำให้เปลี่ยนบรรทัด

Process process = Runtime.getRuntime ().exec ("/bin/bash");

ด้วยเส้น

ProcessBuilder builder = new ProcessBuilder("/bin/bash");
builder.redirectErrorStream(true);
Process process = builder.start();

ProcessBuilder เป็นของใหม่ใน Java 5 และทำให้การรันกระบวนการภายนอกง่ายขึ้น ในความคิดของฉันการปรับปรุงที่สำคัญที่สุดRuntime.getRuntime().exec()คือช่วยให้คุณสามารถเปลี่ยนเส้นทางข้อผิดพลาดมาตรฐานของกระบวนการย่อยไปยังเอาต์พุตมาตรฐานได้ ซึ่งหมายความว่าคุณมีเพียงหนึ่งที่InputStreamจะอ่านจาก ก่อนหน้านี้คุณจำเป็นต้องมีเธรดสองเธรดที่แยกจากกันการอ่านหนึ่งรายการstdoutและการอ่านหนึ่งจากstderrเพื่อหลีกเลี่ยงการเติมบัฟเฟอร์ข้อผิดพลาดมาตรฐานในขณะที่บัฟเฟอร์เอาต์พุตมาตรฐานว่างเปล่า (ทำให้กระบวนการลูกหยุดทำงาน) หรือในทางกลับกัน

ถัดไปลูป (ซึ่งคุณมีสอง)

while ((line = reader.readLine ()) != null) {
    System.out.println ("Stdout: " + line);
}

ออกก็ต่อเมื่อreaderซึ่งอ่านจากเอาต์พุตมาตรฐานของกระบวนการส่งกลับ end-of-file สิ่งนี้จะเกิดขึ้นเมื่อbashกระบวนการออกเท่านั้น มันจะไม่ส่งคืน end-of-file หากเกิดขึ้นในปัจจุบันโดยจะไม่มีเอาต์พุตจากกระบวนการอีกต่อไป แต่จะรอบรรทัดผลลัพธ์ถัดไปจากกระบวนการและไม่กลับมาจนกว่าจะมีบรรทัดถัดไปนี้

เนื่องจากคุณกำลังส่งอินพุตสองบรรทัดไปยังกระบวนการก่อนที่จะถึงลูปนี้ลูปแรกของทั้งสองนี้จะหยุดทำงานหากกระบวนการยังไม่ออกหลังจากอินพุตสองบรรทัดนี้ มันจะนั่งรออีกบรรทัดที่จะอ่าน แต่จะไม่มีบรรทัดอื่นให้อ่าน

ฉันรวบรวมซอร์สโค้ดของคุณ (ฉันใช้ Windows ในขณะนี้ดังนั้นฉันจึงแทนที่/bin/bashด้วยcmd.exeแต่หลักการควรจะเหมือนกัน) และฉันพบว่า:

  • หลังจากพิมพ์สองบรรทัดผลลัพธ์จากสองคำสั่งแรกจะปรากฏขึ้น แต่จากนั้นโปรแกรมก็หยุดทำงาน
  • ถ้าฉันพิมพ์พูดecho testแล้วexitโปรแกรมจะทำให้มันไม่อยู่ในลูปแรกตั้งแต่cmd.exeออกจากกระบวนการ จากนั้นโปรแกรมจะขออินพุตบรรทัดอื่น (ซึ่งถูกเพิกเฉย) ข้ามตรงไปยังลูปที่สองเนื่องจากกระบวนการลูกได้ออกไปแล้วและออกจากตัวเอง
  • ถ้าฉันพิมพ์exitแล้วecho testฉันจะได้รับ IOException บ่นว่าท่อถูกปิด สิ่งนี้เป็นสิ่งที่คาดหวัง - อินพุตบรรทัดแรกทำให้กระบวนการออกและไม่มีที่ไหนที่จะส่งบรรทัดที่สอง

ฉันเคยเห็นกลเม็ดที่ทำสิ่งที่คล้ายกับสิ่งที่คุณต้องการในโปรแกรมที่ฉันเคยทำ โปรแกรมนี้เก็บไว้รอบ ๆ เชลล์จำนวนมากรันคำสั่งในนั้นและอ่านเอาต์พุตจากคำสั่งเหล่านี้ เคล็ดลับที่ใช้คือเขียนบรรทัด 'magic' ที่ทำเครื่องหมายจุดสิ้นสุดของเอาต์พุตของคำสั่งเชลล์และใช้เพื่อกำหนดว่าเมื่อใดที่เอาต์พุตจากคำสั่งที่ส่งไปยังเชลล์เสร็จสิ้น

ฉันเอารหัสของคุณและฉันแทนที่ทุกอย่างหลังบรรทัดที่กำหนดให้writerด้วยลูปต่อไปนี้:

while (scan.hasNext()) {
    String input = scan.nextLine();
    if (input.trim().equals("exit")) {
        // Putting 'exit' amongst the echo --EOF--s below doesn't work.
        writer.write("exit\n");
    } else {
        writer.write("((" + input + ") && echo --EOF--) || echo --EOF--\n");
    }
    writer.flush();

    line = reader.readLine();
    while (line != null && ! line.trim().equals("--EOF--")) {
        System.out.println ("Stdout: " + line);
        line = reader.readLine();
    }
    if (line == null) {
        break;
    }
}

หลังจากทำสิ่งนี้ฉันสามารถเรียกใช้คำสั่งสองสามคำสั่งได้อย่างน่าเชื่อถือและให้ผลลัพธ์จากแต่ละรายการกลับมาหาฉันทีละคำ

echo --EOF--คำสั่งสองคำสั่งในบรรทัดที่ส่งไปยังเชลล์จะอยู่ที่นั่นเพื่อให้แน่ใจว่าเอาต์พุตจากคำสั่งนั้นถูกยกเลิก--EOF--แม้จะเป็นผลลัพธ์ของข้อผิดพลาดจากคำสั่ง

แน่นอนว่าแนวทางนี้มีข้อ จำกัด ข้อ จำกัด เหล่านี้ ได้แก่ :

  • ถ้าฉันป้อนคำสั่งที่รอการป้อนข้อมูลของผู้ใช้ (เช่นเชลล์อื่น) โปรแกรมจะหยุดทำงาน
  • สมมติว่าแต่ละกระบวนการที่รันโดยเชลล์สิ้นสุดเอาต์พุตด้วยการขึ้นบรรทัดใหม่
  • --EOF--มันได้รับบิตสับสนถ้าคำสั่งที่ถูกดำเนินการโดยเปลือกที่เกิดขึ้นกับเขียนออกบรรทัด
  • bash)รายงานข้อผิดพลาดทางไวยากรณ์และออกถ้าคุณใส่ข้อความบางส่วนกับที่ไม่มีใครเทียบ

ประเด็นเหล่านี้อาจไม่สำคัญสำหรับคุณหากสิ่งที่คุณคิดจะทำงานตามกำหนดเวลาจะถูก จำกัด ไว้ที่คำสั่งหรือชุดคำสั่งเล็ก ๆ ซึ่งจะไม่ทำงานในรูปแบบทางพยาธิวิทยาเช่นนั้น

แก้ไข : ปรับปรุงการจัดการการออกและการเปลี่ยนแปลงเล็กน้อยอื่น ๆ หลังจากเรียกใช้สิ่งนี้บน Linux


ขอบคุณสำหรับคำตอบที่ครอบคลุมอย่างไรก็ตามฉันคิดว่าฉันได้ระบุกรณีที่แท้จริงสำหรับปัญหาของฉันแล้ว ทำให้ฉันสนใจในโพสต์ของคุณ stackoverflow.com/questions/3645889/… . ขอขอบคุณ.
James moore

1
การแทนที่ / bin / bash ด้วย cmd ไม่ได้ทำงานเหมือนเดิมจริงๆเนื่องจากฉันสร้างโปรแกรมที่ทำเหมือนกัน แต่มีปัญหากับ bash ใน mycase การเปิดสามเธรดแยกกันสำหรับแต่ละอินพุต / เอาต์พุต / err จะทำงานได้ดีที่สุดโดยไม่มีปัญหาใด ๆ สำหรับคำสั่งโต้ตอบแบบเซสชันที่ยาวนาน
Johnydep


@AlexMills: ความคิดเห็นที่สองของคุณหมายความว่าคุณได้แก้ปัญหาของคุณแล้วหรือยัง? ไม่มีรายละเอียดเพียงพอในความคิดเห็นแรกของคุณที่จะบอกว่าปัญหาคืออะไรและทำไมคุณถึงได้รับข้อยกเว้น 'กะทันหัน'
Luke Woodward

@ ลุคลิงก์ที่ฉันให้ไว้เหนือความคิดเห็นของคุณช่วยแก้ปัญหาของฉันได้
Alexander Mills

4

ฉันคิดว่าคุณสามารถใช้เธรดเช่นเธรดปีศาจเพื่ออ่านอินพุตของคุณและตัวอ่านเอาต์พุตของคุณจะอยู่ในขณะที่วนซ้ำในเธรดหลักเพื่อให้คุณสามารถอ่านและเขียนได้ในเวลาเดียวกันคุณสามารถแก้ไขโปรแกรมของคุณได้ดังนี้:

Thread T=new Thread(new Runnable() {

    @Override
    public void run() {
        while(true)
        {
            String input = scan.nextLine();
            input += "\n";
            try {
                writer.write(input);
                writer.flush();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }

    }
} );
T.start();

และคุณสามารถอ่านจะเหมือนกับข้างบนคือ

while ((line = reader.readLine ()) != null) {
    System.out.println ("Stdout: " + line);
}

ทำให้นักเขียนของคุณเป็นขั้นสุดท้ายมิฉะนั้นจะไม่สามารถเข้าถึงได้โดยคนชั้นใน


1

คุณมีwriter.close();ในรหัสของคุณ ดังนั้น bash จึงได้รับ EOF stdinและออก จากนั้นคุณจะได้รับBroken pipeเมื่อพยายามอ่านจากstdoutbash ที่ตายแล้ว

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