Search code examples
javasynchronizationprocessbuilder

Problems again with child processes in Java


I am on Ubuntu 14.04. I am trying to run something like ps aux | grep whatevah through Java's class ProcessBuilder. I create two child processes and I make them communicate synchronously, but for some reason, I can not see anything in the terminal.

This is the code:

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

(The System.out.println(p1Output); is just for me to check, the print that has to work is the last one, printing the result of ps aux | grep whatevah.)

I've tried several things, the less silly include:

  • If I comment everything regarding process2, I get the result of ps aux printed on the terminal
  • If I run the program as is, it prints nothing to the terminal.
  • If I uncomment the waitFor call, only ps aux gets printed.
  • If change the commands to, for example, ls -al and ls -al, then both get printed.
  • I tried changing "aux" for "aux |" but still nothing is printed.
  • Closed the buffers, also nothing

etc.

Any help will be sorely appreciated. Cheers!

EDIT

Minutes after accepting Ryan's amazing answer I made my last try to make this code work. And I succeeded! I changed:

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

for:

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

bw.close();

and it works! I remember closing the buffer before, so I don't know what went wrong. Turns out you should not stay awake until late trying to make a piece of code work XD.

Ryan's answer down here is still amazing, though.


Solution

  • Given the advice in the comments, the important thing to note is the necessity to use threads to process input/output for a process in order to achieve what you want.

    I've used the link posted by jtahlborn and adapted this solution that you might be able to use.

    I created a simple example that will list files in a directory and grep through the output. This example simulates the command ls -1 | grep some from a directory called test with three files somefile.txt someotherfile.txt and this_other_file.csv

    EDIT: The original solution didn't really fully use the "pipe" methodology, as it was waiting fully for p1 to finish before starting p2. Rather, it should start them both, and then the output of the first should be piped to the second. I've updated the solution with a class that accomplishes this.

    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 is a class that will be used to simulate a process pipe. It uses some loops to copy bytes around, and could be more efficient, depending on your needs, but for the illustration, it should work.

    // 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 is a class that was created for handling process output, adapted from 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);
        }
    }
    

    The produced output is:

    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
    

    To show that piping is really working, changing the command to ps -a | grep usr the output is:

    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
    

    Seeing the grep command in process 2's output shows that the piping is working, with the old solution I posted, this would be missing.

    Note the handling of the error stream, which is always good practice, even if you don't plan to use it.

    This is a quick and dirty solution that could benefit from some additional thread management techniques, but it should get you what you want.