【问题标题】:Problems again with child processes in JavaJava中的子进程再次出现问题
【发布时间】:2014-10-14 16:27:28
【问题描述】:

我在 Ubuntu 14.04 上。 我正在尝试通过 Java 的类 ProcessBuilder 运行类似 ps aux | grep whatevah 的东西。我创建了两个子进程并让它们同步通信,但由于某种原因,我在终端中看不到任何东西。

这是代码:

try {
    // What comes out of process1 is our inputStream
    Process process1   = new ProcessBuilder("ps", "aux").start();
    InputStream is1    = process1.getInputStream();
    BufferedReader br1 = new BufferedReader (new InputStreamReader(is1));

    // What goes into process2 is our outputStream
    Process process2  = new ProcessBuilder("grep", "gedit").start();
    OutputStream os   = process2.getOutputStream();
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));

    // Send the output of process1 to the input of process2
    String p1Output  = null;
    while ((p1Output = br1.readLine()) != null) {
        bw.write(p1Output);
        System.out.println(p1Output);
    }
    // Synchronization
    int finish = process2.waitFor();
    System.out.println(finish);

    // What comes out of process2 is our inputStream            
    InputStream is2    = process2.getInputStream();
    BufferedReader br2 = new BufferedReader(new InputStreamReader(is2));

    String combOutput  = null;
    while ((combOutput = br2.readLine()) != null)
        System.out.println(combOutput);

    os.close();
    is1.close();
    is2.close();

} catch (IOException e) {
    System.out.println("Command execution error: " + e.getMessage());
} catch (Exception e) {
    System.out.println("General error: " + e.getMessage());
}

System.out.println(p1Output);是我自己检查的,要工作的打印是最后一个,打印ps aux | grep whatevah的结果。)

我尝试了几件事,不太傻的包括:

  • 如果我评论有关 process2 的所有内容,我会在终端上打印ps aux 的结果
  • 如果我按原样运行程序,它不会向终端打印任何内容。
  • 如果我取消注释 waitFor 调用,则只会打印 ps aux
  • 例如,如果将命令更改为 ls -alls -al,则两者都会被打印。
  • 我尝试将"aux" 更改为"aux |",但仍然没有打印任何内容。
  • 关闭了缓冲区,什么也没有

等等

非常感谢任何帮助。 干杯!

编辑

在接受 Ryan 的惊人回答几分钟后,我最后一次尝试让这段代码工作。而我成功了!我改变了:

while ((p1Output = br1.readLine()) != null) {
    bw.write(p1Output);
    System.out.println(p1Output);
}

为:

while ((p1Output = br1.readLine()) != null) {
    bw.write(p1Output + "\n");
    System.out.println(p1Output);
}

bw.close();

它有效!我记得之前关闭了缓冲区,所以我不知道出了什么问题。事实证明,你不应该熬夜到很晚才试图让一段代码工作 XD。

不过,Ryan 在下面的回答仍然令人惊叹。

【问题讨论】:

  • 可能是因为您需要单独的线程来处理进程的输入和输出。相关文章here。第一个循环很可能是阻塞的,因为您没有及时使用 grep 输出。
  • @jtahlborn 抱歉,您说“您需要单独的线程来处理输入和输出”是什么意思?另外,您链接的文章是为Runtime... 是否也适用于PB?
  • 这篇文章解释了为什么线程是必要的。 Runtime.exec 只是 ProcessBuilder 的一个快捷方式。

标签: java synchronization processbuilder


【解决方案1】:

鉴于 cmets 中的建议,需要注意的重要一点是必须使用线程来处理进程的输入/输出,以实现您想要的。

我使用了 jtahlborn 发布的链接,并调整了您可能可以使用的解决方案。

我创建了一个简单的示例,该示例将列出目录中的文件并通过输出进行 grep。 此示例使用三个文件 somefile.txt someotherfile.txtthis_other_file.csv 模拟来自名为 test 的目录中的命令 ls -1 | grep some

编辑:最初的解决方案并没有真正完全使用“管道”方法,因为它在启动 p2 之前完全等待 p1 完成。相反,它应该同时启动它们,然后第一个的输出应该通过管道传输到第二个。我已经使用完成此任务的类更新了解决方案。

import java.io.*;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        try {
            // construct a process
            ProcessBuilder pb1 = new ProcessBuilder("ls", "-1");
            // set working directory
            pb1.directory(new File("test"));

            // start process
            final Process process1 = pb1.start();

            // get input/error streams
            final InputStream p1InStream = process1.getInputStream();
            final InputStream p1ErrStream = process1.getErrorStream();

            // handle error stream
            Thread t1Err = new InputReaderThread(p1ErrStream, "Process 1 Err");
            t1Err.start();

            // this will print out the data from process 1 (for illustration purposes)
            // and redirect it to process 2
            Process process2  = new ProcessBuilder("grep", "some").start();

            // process 2 streams
            final InputStream p2InStream = process2.getInputStream();
            final InputStream p2ErrStream = process2.getErrorStream();
            final OutputStream p2OutStream = process2.getOutputStream();

            // do the same as process 1 for process 2...
            Thread t2In = new InputReaderThread(p2InStream, "Process 2 Out");
            t2In.start();
            Thread t2Err = new InputReaderThread(p2ErrStream, "Process 2 Err");
            t2Err.start();

            // create a new thread with our pipe class
            // pass in the input stream of p1, the output stream of p2, and the name of the input stream
            new Thread(new PipeClass(p1InStream, p2OutStream, "Process 1 Out")).start();

            // wait for p2 to finish
            process2.waitFor();

        } catch (IOException e) {
            System.out.println("Command execution error: " + e.getMessage());
        } catch (Exception e) {
            System.out.println("General error: " + e.getMessage());
        }
    }
}

这是一个用于模拟流程管道的类。它使用一些循环来复制字节,并且可能更有效,具体取决于您的需要,但为了说明,它应该可以工作。

// this class simulates a pipe between two processes
public class PipeClass implements Runnable {
    // the input stream
    InputStream is;
    // the output stream
    OutputStream os;
    // the name associated with the input stream (for printing purposes only...)
    String isName;

    // constructor
    public PipeClass(InputStream is, OutputStream os, String isName) {
        this.is = is;
        this.os = os;
        this.isName = isName;
    }

    @Override
    public void run() {
        try {
            // use a byte array output stream so we can clone the data and use it multiple times
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            // read the data into the output stream (it has to fit in memory for this to work...)
            byte[] buffer = new byte[512]; // Adjust if you want
            int bytesRead;
            while ((bytesRead = is.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesRead);
            }

            // clone it so we can print it out
            InputStream clonedIs1 = new ByteArrayInputStream(baos.toByteArray());
            Scanner sc = new Scanner(clonedIs1);

            // print the info
            while (sc.hasNextLine()) {
                System.out.println(this.isName + " >> " + sc.nextLine());
            }

            // clone again to redirect to the output of the other process
            InputStream clonedIs2 = new ByteArrayInputStream(baos.toByteArray());
            buffer = new byte[512]; // Adjust if you want
            while ((bytesRead = clonedIs2.read(buffer)) != -1) {
                // write it out to the output stream
                os.write(buffer, 0, bytesRead);
            }
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }
        finally {
            try {
                // close so the process will finish
                is.close();
                os.close();
            }
            catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

这是一个为处理流程输出而创建的类,改编自this reference

// Thread reader class adapted from
// http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html
public class InputReaderThread extends Thread {
    // input stream
    InputStream is;
    // name
    String name;
    // is there data?
    boolean hasData = false;
    // data itself
    StringBuilder data = new StringBuilder();


    // constructor
    public InputReaderThread(InputStream is, String name) {
        this.is = is;
        this.name = name;
    }

    // set if there's data to read
    public synchronized void setHasData(boolean hasData) {
        this.hasData = hasData;
    }

    // data available?
    public boolean hasData() { return this.hasData; }

    // get the data
    public StringBuilder getData() {
        setHasData(false);  // clear flag
        StringBuilder returnData = this.data;
        this.data = new StringBuilder();

        return returnData;
    }

    @Override
    public void run() {
        // input reader
        InputStreamReader isr = new InputStreamReader(this.is);
        Scanner sc = new Scanner(isr);
        // while data remains
        while ( sc.hasNextLine() ) {
            // print out and append to data
            String line = sc.nextLine();
            System.out.println(this.name + " >> " + line);
            this.data.append(line + "\n");
        }
        // flag there's data available
        setHasData(true);
    }
}

产生的输出是:

Process 1 Out >> somefile.txt
Process 1 Out >> someotherfile.txt
Process 1 Out >> this_other_file.csv
Process 2 Out >> somefile.txt
Process 2 Out >> someotherfile.txt

为了表明管道确实有效,将命令更改为ps -a | grep usr,输出为:

Process 1 Out >>       PID    PPID    PGID     WINPID  TTY  UID    STIME COMMAND
Process 1 Out >> I   15016       1   15016      15016  con  400 13:45:59 /usr/bin/grep
Process 1 Out >>     15156       1   15156      15156  con  400 14:21:54 /usr/bin/ps
Process 1 Out >> I    9784       1    9784       9784  con  400 14:21:54 /usr/bin/grep
Process 2 Out >> I   15016       1   15016      15016  con  400 13:45:59 /usr/bin/grep
Process 2 Out >>     15156       1   15156      15156  con  400 14:21:54 /usr/bin/ps
Process 2 Out >> I    9784       1    9784       9784  con  400 14:21:54 /usr/bin/grep

在进程 2 的输出中看到 grep 命令表明管道正在工作,使用我发布的旧解决方案,这将丢失。

注意错误流的处理,这始终是一种好习惯,即使您不打算使用它。

这是一个快速而肮脏的解决方案,可以从一些额外的线程管理技术中受益,但它应该可以得到你想要的。

【讨论】:

  • 这个解决方案的问题是你在调用第二个进程之前缓冲了第一个进程的全部结果。相反,您希望将第一个进程的结果流式传输到一个线程中的第二个进程,同时从第二个线程中读取第二个进程的结果。
  • 是的,我注意到这将是一个限制,并努力找出更好的方法来处理它。我已经更新了我的答案以反映它。感谢您的反馈
  • 哇,我从没想过这会变得如此复杂。你的回答和文章令人兴奋。我很想知道为什么网络上关于这个主题的信息这么少,而最好的文章是 2000 年的?人们根本不这样做吗?没有用例吗?他们是否使用了不同的方法?
  • 进程管理和重定向本身是非常基本的。它只是对文件句柄的操作。可能您没有找到大量当前信息和解决方案的原因可能是因为如果您可以避免它,通常会受到反对,因为您需要注意很多事情。让shell为您完成工作并简单地使用脚本来管道您的命令可能会更容易,然后您不必处理它。流程通信的使用将非常适合您的特定需求,这就是您找不到“通用”解决方案的原因
  • @mestevens - 没有任何其他文章 (IMO) 的原因是因为那篇文章非常有名,并且几乎很好地总结了所有内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-03-18
  • 1970-01-01
  • 1970-01-01
  • 2022-08-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多