Java输入/输出流的进程处理

102
我有以下的代码示例,它可以让您输入一个命令到bash shell,例如echo test,然后将结果回显。但是,第一次读取之后,其他输出流无法正常工作?
这是为什么?还是我做错了什么?我的最终目标是创建一个定期执行命令到/bash的线程任务,因此OutputStreamInputStream必须一起工作,而不是停止工作。我也遇到了错误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);
}

“Broken pipe” 可能意味着子进程已退出。我还没有完全查看你的代码,看看还有哪些问题。 - vanza
1
使用单独的线程,它会正常工作。 - Johnydep
3个回答

149

首先,我建议更换这一行代码

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

使用这些行

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

ProcessBuilder是Java 5中新增的功能,可以更轻松地运行外部进程。在我看来,其最显著的改进之一是允许将子进程的标准错误重定向到其标准输出。这意味着您只需要从一个InputStream中读取数据。在此之前,您需要有两个单独的线程,一个从stdout读取数据,另一个从stderr读取数据,以避免标准错误缓冲区填满而标准输出缓冲区为空(导致子进程挂起),或者反之。
接下来,循环(您有两个)。
while ((line = reader.readLine ()) != null) {
    System.out.println ("Stdout: " + line);
}

只有当读取进程的标准输出流的 reader 返回文件末尾时,才会退出。如果当前进程没有更多的输出,那么它不会返回文件末尾。相反,它将等待来自进程的下一行输出,直到读取到这行输出后才会返回。
由于在进入此循环之前向进程发送了两行输入,如果该进程在这两行输入后没有退出,则第一个循环将挂起。它会一直等待另一行要读取的内容,但是却永远没有需要读取的内容。
我已经编译了您的源代码(目前我在Windows上,所以我用 cmd.exe 替换了 /bin/bash,但原则应该是相同的),并发现:
  • 在输入两条命令后,前两个命令的输出会出现,但之后程序会挂起,
  • 例如我输入 echo test 然后输入 exit,由于 cmd.exe 进程已退出,程序走出了第一个循环。程序然后要求再输入另一行(这会被忽略),跳过第二个循环,因为子进程已经退出了,随后程序也退出了。
  • 如果我输入 exit 然后输入 echo test,我会收到一个IOException,提示管道已关闭。这是可以预料的 —— 第一行输入导致进程退出了,没有地方去发送第二行输入了。
我曾经在一份我曾经工作过的程序中看过一种类似于您所需的技巧。该程序保留了许多shell,在其中运行命令并读取这些命令的输出。使用的技巧是始终写出一个 'magic' 行,标记 shell 命令输出的末尾,并使用它来确定发往shell的命令的输出何时结束。
我拿了您的代码,并将 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;
    }
}

完成此操作后,我可以可靠地运行一些命令,并使每个命令的输出单独返回给我。
发送到shell的行中的两个echo --EOF--命令用于确保即使在命令出错的情况下也会以--EOF--终止命令的输出。
当然,这种方法有其局限性。这些限制包括:
  • 如果我输入等待用户输入的命令(例如另一个shell),则程序似乎挂起,
  • 它假定由shell运行的每个进程都以换行符结束其输出,
  • 如果正在运行shell的命令恰好写出一行--EOF--,它会变得有点混乱。
  • 如果您输入具有不匹配的)的文本,则bash会报告语法错误并退出。
如果您考虑作为计划任务运行的任何内容将被限制为命令或一小组永远不会表现出这种病态方式的命令,则这些要点可能对您无关紧要。 编辑:改进退出处理和其他次要更改,以便在Linux上运行时进行更正。

1
将/bin/bash替换为cmd并不会像我编写的程序一样表现相同,因为它在bash中存在问题。在我的情况下,为每个输入/输出/错误打开三个单独的线程对于长时间会话交互命令而言是最好的选择,没有任何问题。 - Johnydep
https://dev59.com/YW7Xa4cB1Zd3GeqPnThg - Alexander Mills
@AlexMills:你的第二条评论是不是意味着你已经解决了问题?在你的第一条评论中,没有足够的细节说明问题所在,也没有说明为什么会“突然”出现异常。 - Luke Woodward
@Luke,我在你的评论上方提供的链接解决了我的问题。 - Alexander Mills
@LukeWoodward 但是我怎么知道是否出现了错误?假设我运行一个可执行的C程序。 - alhelal
显示剩余5条评论

5

我认为您可以使用类似于守护线程的线程来读取输入,而您的输出读取器已经在主线程的while循环中,因此您可以同时读取和写入。您可以像这样修改您的程序:

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

将您的写入器设置为final,否则内部类将无法访问它。


1

你的代码中有 writer.close();。因此,bash 在其 stdin 上接收到 EOF 并退出。然后,当尝试从已死的 bash 的 stdout 读取时,你会得到 Broken pipe


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接