Saturday, June 4, 2011

Java: From Runtime.exec() to ProcessBuilder

Before JDK 5.0, the only way to fork off a process and execute it local to the user runtime was to use the exec() method of the java.lang.Runtime class. JDK 5.0 adds a new way of executing a command in a separate process, through a class called ProcessBuilder. You can find ProcessBuilder in the java.lang package (like Runtime and Process). This tip discusses and compares both approaches.

If you're familiar with the Runtime class, you know that it also allows you to discover memory usage and add a shutdown hook. But probably the most popular use of the class prior to 5.0 was to execute a command in a separate process. This was done through one of the six versions of the exec() method of Runtime:

public Process exec(String command)
             throws IOException

public Process exec(String command,
                    String[] envp)
             throws IOException

public Process exec(String command,
                    String[] envp,
                    File dir)
             throws IOException

public Process exec(String[] cmdarray)
             throws IOExceptionjava

public Process exec(String[] cmdarray,
                    String[] envp)
             throws IOException

public Process exec(String[] cmdarray,
                    String[] envp,
                    File dir)
             throws IOException

Before you call the exec() method, you specify the command and its arguments, environment variable settings, and working directory. All versions of the method return a java.lang.Process object for managing the created process. This allows you to get the input or output stream of the subprocess and exit status (among other available information).

Here's an example, DoRuntime, that shows how to execute a command with the original Runtime class. The command to run is passed in from the command line.

   import java.io.*;
   import java.util.*;
   
   public class DoRuntime {
     public static void main(String args[]) throws IOException {

       if (args.length <= 0) {
         System.err.println("Need command to run");
         System.exit(-1);
       }

       Runtime runtime = Runtime.getRuntime();
       Process process = runtime.exec(args);
       InputStream is = process.getInputStream();
       InputStreamReader isr = new InputStreamReader(is);
       BufferedReader br = new BufferedReader(isr);
       String line;

       System.out.printf("Output of running %s is:"
           Arrays.toString(args));

       while ((line = br.readLine()) != null) {
         System.out.println(line);
       }

     }
    

If you run DoRuntime in Solaris like this:

  java DoRuntime ls

You get output that looks something like this (which depends on the contents of the directory):
  Output of running ls is:DoRuntime.class
  DoRuntime.java  

Linux users could also pass in "ls" as the command to get a directory listing.

On a Microsoft Windows platform, commands such as "dir" are internal to the command processor so the single command-line argument would be the quoted string: "cmd /c dir" (again, output would depend on the contents of the directory).

  >  java DoRuntime "cmd /c dir"

  Output of running cmd /c dir is: ...

  Directory of C:\...

  07/15/2005  09:30 AM    <DIR>          .
  07/15/2005  09:30 AM    <DIR>          ..
  07/15/2005  09:30 AM             1,146 DoRuntime.class
  07/15/2005  09:23 AM               724 DoRuntime.java
  ...
As coded, the command executes in the current working directory with its environment variables intact.
If you want to run the command in a different directory, and you need to add more arguments to the exec() command, you change:
    Runtime runtime = Runtime.getRuntime();
    Process process = runtime.exec(command);
to:
    File file = new File(other directory);
    Runtime runtime = Runtime.getRuntime();
    Process process = runtime.exec(command, null, file);

The second parameter in the call to the exec() method identifies the environment variable settings. Because the parameter is "null", the subprocess inherits the environment settings of the current process.

So what's wrong with this approach? Why create a new approach? The problem is that the Runtime.exec approach doesn't necessarily make it easy to customize and invoke subprocesses. The new ProcessBuilder class simplifies things. Through various methods in the class, you can easily modify the environment variables for a process and start the process.

Here's a simple use of ProcessBuilder that duplicates the functions of the DoRuntime example:
   import java.io.*;
   import java.util.*;
   
   public class DoProcessBuilder {
     public static void main(String args[]) throws IOException {

       if (args.length <= 0) {
         System.err.println("Need command to run");
         System.exit(-1);
       }

       Process process = new ProcessBuilder(args).start();
       InputStream is = process.getInputStream();
       InputStreamReader isr = new InputStreamReader(is);
       BufferedReader br = new BufferedReader(isr);
       String line;

       System.out.printf("Output of running %s is:"
          Arrays.toString(args));

       while ((line = br.readLine()) != null) {
         System.out.println(line);
       }

     }
    }
 
  > java DoProcessBuilder ls
  Output of running ls is:DoProcessBuilder.class
  DoProcessBuilder.java
  DoRuntime.class
  DoRuntime.java  

Notice that the following two lines in DoRuntime:

    Runtime runtime = Runtime.getRuntime();
    Process process = runtime.exec(command);

were changed to the following line in DoProcessBuilder:

Process process = new ProcessBuilder(command).start();

The ProcessBuilder class has two constructors. One constructor accepts a List for the command and its arguments. The other constructor accepts a variable number of String arguments.
   public ProcessBuilder(List command)
   public ProcessBuilder(String... command)

With ProcessBuilder, you call start() to execute the command. Prior to calling start(), you can manipulate how the Process will be created. If you want the process to start in a different directory, you don't pass a File in as a command line argument. Instead, you set the process builder's working directory by passing the File to the directory() method:

public ProcessBuilder directory(File directory)

There isn't an obvious setter type method in ProcessBuilder for setting environment variables. Instead, you get a Map of the variables through the environment() method, then you manipulate the Map:

ProcessBuilder processBuilder = new ProcessBuilder(command);
Map env = processBuilder.environment();
// manipulate env

The options for manipulating the environment include adding environment variables with the put() method, and removing them with the remove() method. For example:

   ProcessBuilder processBuilder = new ProcessBuilder(
                                       command, arg1, arg2);
   Map env = processBuilder.environment();
   env.put("var1", "value");
   env.remove("var3");

After the environment variables and directory are set, call start():
   processBuilder.directory("Dir");
   Process p = processBuilder.start();

You can also clear() all the variables from the environment and explicitly set the ones you want.

With methods such as environment() for adding and removing environment variables from the process space, and start() for starting a new process, ProcessBuilder should make it easier to invoke a subprocess with a modified process environment.
You can get the initial set of environment variables by calling the getenv() method of System. 

Understand that not all platforms support changing environment variables. If you try to change an environment variable on a platform that forbids it, the operation will throw either an UnsupportedOperationException or an IllegalArgumentException. Also, when running with a security manager, you'll need the RuntimePermission for "getenv.*", otherwise a SecurityException will be thrown.

Remember not to forget the start() call after configuring your instance. And, keep using the Process class to manipulate the streams for the process and to get its exit status.

A word of caution about the examples in this tip. It is possible that the examples will deadlock if the subprocess generates enough output to overflow the system. A more robust solution requires draining the process stdout and stderr in separate threads.

For more information about ProcessBuilder, see the class definition

No comments :

Post a Comment