5

I want to use an external tool while extracting some data (loop through lines). For that I first used Runtime.getRuntime().exec() to execute it. But then my extraction got really slow. So I am searching for a possibility to exec the external tool in each instance of the loop, using the same instance of shell.

I found out, that I should use ProcessBuilder. But it's not working yet.

Here is my code to test the execution (with input from the answers here in the forum already):

public class ExecuteShell {
   ProcessBuilder builder;
   Process process = null;
   BufferedWriter process_stdin;
   BufferedReader reader, errReader;

   public ExecuteShell() {
    String command;
    command = getShellCommandForOperatingSystem();
    if(command.equals("")) {
        return; //Fehler!  No error handling yet
    }
    //init shell
    builder = new ProcessBuilder( command);
    builder.redirectErrorStream(true);
    try {
        process = builder.start();
    } catch (IOException e) {
        System.out.println(e);
    }

    //get stdout of shell  
    reader    = new BufferedReader(new InputStreamReader(process.getInputStream()));  
    errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));

    //get stdin of shell
    process_stdin = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
    System.out.println("ExecuteShell: Constructor successfully finished");
   }

   public String executeCommand(String commands) {
    StringBuffer output;
    String line;
    try {
        //single execution
        process_stdin.write(commands);
        process_stdin.newLine();
        process_stdin.flush();
    } catch (IOException e) {
        System.out.println(e);
    }
    output    = new StringBuffer();
    line = ""; 

    try {
        if (!reader.ready()) {
            output.append("Reader empty \n");
            return output.toString();
        }
        while ((line = reader.readLine())!= null) {
            output.append(line + "\n");
            return output.toString();
        }
        if (!reader.ready()) {
            output.append("errReader empty \n");
            return output.toString();
        }
        while ((line = errReader.readLine())!= null) {
            output.append(line + "\n");
        }
    } catch (Exception e) {
        System.out.println("ExecuteShell: error in executeShell2File");
        e.printStackTrace();
        return "";
    }
    return output.toString();
   }


   public int close() {
    // finally close the shell by execution exit command
    try {
        process_stdin.write("exit");
        process_stdin.newLine();
        process_stdin.flush();
    }
    catch (IOException e) {
        System.out.println(e);
        return 1;
    }
    return 0;
   }

   private static String getShellCommandForOperatingSystem() {
    Properties prop = System.getProperties( );
    String os =  prop.getProperty( "os.name" );
    if ( os.startsWith("Windows") ) {
        //System.out.println("WINDOWS!");
        return "C:/cygwin64/bin/bash";
    } else if (os.startsWith("Linux") ) { 
        //System.out.println("Linux!");
        return"/bin/sh";
    }
    return "";      
   }
}

I want to call it in another Class like this Testclass:

public class TestExec{
    public static void main(String[] args) {
        String result = "";
        ExecuteShell es = new ExecuteShell();
        for (int i=0; i<5; i++) {
          // do something
          result = es.executeCommand("date"); //execute some command
          System.out.println("result:\n" + result); //do something with result
          // do something
        }
        es.close();
    }
}

My Problem is, that the output stream is always empty:

ExecuteShell: Constructor successfully finished
result:
Reader empty 

result:
Reader empty 

result:
Reader empty 

result:
Reader empty 

result:
Reader empty 

I read the thread here: Java Process with Input/Output Stream

But the code snippets were not enough to get me going, I am missing something. I have not really worked with different threads much. And I am not sure if/how a Scanner is of any help to me. I would really appreciate some help.

Ultimatively, my goal is to call an external command repeatetly and make it fast.

EDIT: I changed the loop, so that the es.close() is outside. And I wanted to add, that I do not want only this inside the loop.

EDIT: The problem with the time was, that the command I called caused an error. When the command does not cause an error, the time is acceptable.

Thank you for your answers

Community
  • 1
  • 1
emi-le
  • 756
  • 9
  • 26

1 Answers1

1

You are probably experiencing a race condition: after writing the command to the shell, your Java program continues to run, and almost immediately calls reader.ready(). The command you wanted to execute has probably not yet output anything, so the reader has no data available. An alternative explanation would be that the command does not write anything to stdout, but only to stderr (or the shell, maybe it has failed to start the command?). You are however not reading from stderr in practice.

To properly handle output and error streams, you cannot check reader.ready() but need to call readLine() (which waits until data is available) in a loop. With your code, even if the program would come to that point, you would read only exactly one line from the output. If the program would output more than one line, this data would get interpreted as the output of the next command. The typical solution is to read in a loop until readLine() returns null, but this does not work here because this would mean your program would wait in this loop until the shell terminates (which would never happen, so it would just hang infinitely). Fixing this would be pretty much impossible, if you do not know exactly how many lines each command will write to stdout and stderr.

However, your complicated approach of using a shell and sending commands to it is probably completely unnecessary. Starting a command from within your Java program and from within the shell is equally fast, and much easier to write. Similarly, there is no performance difference between Runtime.exec() and ProcessBuilder (the former just calls the latter), you only need ProcessBuilder if you need its advanced features.

If you are experiencing performance problems when calling external programs, you should find out where they are exactly and try to solve them, but not with this approach. For example, normally one starts a thread for reading from both the output and the error stream (if you do not start separate threads and the command produces large output, everything might hang). This could be slow, so you could use a thread pool to avoid repeated spawning of processes.

Philipp Wendler
  • 11,184
  • 7
  • 52
  • 87
  • 1
    To add: It's generally best not to use Java as a replacement for bash. – Ian McLaird Jan 12 '15 at 19:14
  • Hello Phillip, thank you for your answer. When I didn't check if reader was ready. There was no response at all (I even waited some minutes.....). That's why I added the return statement in the while loop. Because I first thought, that maybe it was hang up there (I just forgot about it, when I posted my code here). I just do not get the output of the shell. I think the command has not really started. What do you mean by "Starting a command from within your Java program and from within the shell is equally fast, and much easier to write"? Do you mean a bash script instead of a java program? – emi-le Jan 13 '15 at 08:00
  • I mean that you should not bother with the shell at all and instead run the command you want to execute (in your case it is `date`) directly with `Runtime.exec()` or `ProcessBuilder`. – Philipp Wendler Jan 13 '15 at 08:19
  • I want to call cs2cs and therefore need to inclued a pipe or < in my command. Thats how I got to using the shell (was before christmas, so I forgot). I could not make the input and output streams working, while trying to do something like explained here: http://stackoverflow.com/questions/11336157/running-external-program-with-redirected-stdin-and-stdout-from-java (Same problem as above. The program is hanging, getting no output) – emi-le Jan 13 '15 at 10:49
  • If you need a pipe (`|`), you really need a shell. However, I suggest to start a new shell each time with something like `/bin/sh -c 'command | command'`. The overhead of starting the shell in addition to the commands should be negligible and it makes things much easier for you. – Philipp Wendler Jan 13 '15 at 18:09