As part of the Java language, the java.lang
package is implicitly imported into every Java program. This package's pitfalls surface often, affecting most programmers. This month, I'll discuss the traps lurking in the Runtime.exec()
method.
Pitfall 4: When Runtime.exec() won't
The class java.lang.Runtime
features a static method called getRuntime()
, which retrieves the current Java Runtime Environment. That is the only way to obtain a reference to the Runtime
object. With that reference, you can run external programs by invoking the Runtime
class's exec()
method. Developers often call this method to launch a browser for displaying a help page in HTML.
There are four overloaded versions of the exec()
command:
public Process exec(String command);
public Process exec(String [] cmdArray);
public Process exec(String command, String [] envp);
public Process exec(String [] cmdArray, String [] envp);
For each of these methods, a command -- and possibly a set of arguments -- is passed to an operating-system-specific function call. This subsequently creates an operating-system-specific process (a running program) with a reference to a Process
class returned to the Java VM. The Process
class is an abstract class, because a specific subclass of Process
exists for each operating system.
You can pass three possible input parameters into these methods:
- A single string that represents both the program to execute and any arguments to that program
- An array of strings that separate the program from its arguments
- An array of environment variables
Pass in the environment variables in the form name=value
. If you use the version of exec()
with a single string for both the program and its arguments, note that the string is parsed using white space as the delimiter via the StringTokenizer
class.
Stumbling into an IllegalThreadStateException
The first pitfall relating to Runtime.exec()
is the IllegalThreadStateException
. The prevalent first test of an API is to code its most obvious methods. For example, to execute a process that is external to the Java VM, we use the exec()
method. To see the value that the external process returns, we use the exitValue()
method on the Process
class. In our first example, we will attempt to execute the Java compiler (javac.exe
):
Listing 4.1 BadExecJavac.java
import java.util.*; import java.io.*; public class BadExecJavac { public static void main(String args[]) { try { Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("javac"); int exitVal = proc.exitValue(); System.out.println("Process exitValue: " + exitVal); } catch (Throwable t) { t.printStackTrace(); } } }
A run of BadExecJavac
produces:
E:\classes\com\javaworld\jpitfalls\article2>java BadExecJavac java.lang.IllegalThreadStateException: process has not exited at java.lang.Win32Process.exitValue(Native Method) at BadExecJavac.main(BadExecJavac.java:13)
If an external process has not yet completed, the exitValue()
method will throw an IllegalThreadStateException
; that's why this program failed. While the documentation states this fact, why can't this method wait until it can give a valid answer?
A more thorough look at the methods available in the Process
class reveals a waitFor()
method that does precisely that. In fact, waitFor()
also returns the exit value, which means that you would not use exitValue()
and waitFor()
in conjunction with each other, but rather would choose one or the other. The only possible time you would use exitValue()
instead of waitFor()
would be when you don't want your program to block waiting on an external process that may never complete. Instead of using the waitFor()
method, I would prefer passing a boolean parameter called waitFor
into the exitValue()
method to determine whether or not the current thread should wait. A boolean would be more beneficial because exitValue()
is a more appropriate name for this method, and it isn't necessary for two methods to perform the same function under different conditions. Such simple condition discrimination is the domain of an input parameter.
Therefore, to avoid this trap, either catch the IllegalThreadStateException
or wait for the process to complete.
Now, let's fix the problem in Listing 4.1 and wait for the process to complete. In Listing 4.2, the program again attempts to execute javac.exe
and then waits for the external process to complete:
Listing 4.2 BadExecJavac2.java
import java.util.*; import java.io.*; public class BadExecJavac2 { public static void main(String args[]) { try { Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("javac"); int exitVal = proc.waitFor(); System.out.println("Process exitValue: " + exitVal); } catch (Throwable t) { t.printStackTrace(); } } }
Unfortunately, a run of BadExecJavac2
produces no output. The program hangs and never completes. Why does the javac
process never complete?
Why Runtime.exec() hangs
The JDK's Javadoc documentation provides the answer to this question:
Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, and even deadlock.
Is this just a case of programmers not reading the documentation, as implied in the oft-quoted advice: read the fine manual (RTFM)? The answer is partially yes. In this case, reading the Javadoc would get you halfway there; it explains that you need to handle the streams to your external process, but it does not tell you how.
Another variable is at play here, as is evident by the large number of programmer questions and misconceptions concerning this API in the newsgroups: though Runtime.exec()
and the Process APIs seem extremely simple, that simplicity is deceiving because the simple, or obvious, use of the API is prone to error. The lesson here for the API designer is to reserve simple APIs for simple operations. Operations prone to complexities and platform-specific dependencies should reflect the domain accurately. It is possible for an abstraction to be carried too far. The JConfig
library provides an example of a more complete API to handle file and process operations (see Resources below for more information).
Now, let's follow the JDK documentation and handle the output of the javac
process. When you run javac
without any arguments, it produces a set of usage statements that describe how to run the program and the meaning of all the available program options. Knowing that this is going to the stderr
stream, you can easily write a program to exhaust that stream before waiting for the process to exit. Listing 4.3 completes that task. While this approach will work, it is not a good general solution. Thus, Listing 4.3's program is named MediocreExecJavac
; it provides only a mediocre solution. A better solution would empty both the standard error stream and the standard output stream. And the best solution would empty these streams simultaneously (I'll demonstrate that later).
Listing 4.3 MediocreExecJavac.java
import java.util.*; import java.io.*; public class MediocreExecJavac { public static void main(String args[]) { try { Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("javac"); InputStream stderr = proc.getErrorStream(); InputStreamReader isr = new InputStreamReader(stderr); BufferedReader br = new BufferedReader(isr); String line = null; System.out.println("<ERROR>"); while ( (line = br.readLine()) != null) System.out.println(line); System.out.println("</ERROR>"); int exitVal = proc.waitFor(); System.out.println("Process exitValue: " + exitVal); } catch (Throwable t) { t.printStackTrace(); } } }
A run of MediocreExecJavac
generates:
E:\classes\com\javaworld\jpitfalls\article2>java MediocreExecJavac <ERROR> Usage: javac <options> <source files> where <options> includes: -g Generate all debugging info -g:none Generate no debugging info -g:{lines,vars,source} Generate only some debugging info -O Optimize; may hinder debugging or enlarge class files -nowarn Generate no warnings -verbose Output messages about what the compiler is doing -deprecation Output source locations where deprecated APIs are used -classpath <path> Specify where to find user class files -sourcepath <path> Specify where to find input source files -bootclasspath <path> Override location of bootstrap class files -extdirs <dirs> Override location of installed extensions -d <directory> Specify where to place generated class files -encoding <encoding> Specify character encoding used by source files -target <release> Generate class files for specific VM version </ERROR> Process exitValue: 2
So, MediocreExecJavac
works and produces an exit value of 2
. Normally, an exit value of 0
indicates success; any nonzero value indicates an error. The meaning of these exit values depends on the particular operating system. A Win32 error with a value of 2
is a "file not found" error. That makes sense, since javac
expects us to follow the program with the source code file to compile.
Thus, to circumvent the second pitfall -- hanging forever in Runtime.exec()
-- if the program you launch produces output or expects input, ensure that you process the input and output streams.
Assuming a command is an executable program
Under the Windows operating system, many new programmers stumble upon Runtime.exec()
when trying to use it for nonexecutable commands like dir
and copy
. Subsequently, they run into Runtime.exec()
's third pitfall. Listing 4.4 demonstrates exactly that:
Listing 4.4 BadExecWinDir.java
import java.util.*; import java.io.*; public class BadExecWinDir { public static void main(String args[]) { try { Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("dir"); InputStream stdin = proc.getInputStream(); InputStreamReader isr = new InputStreamReader(stdin); BufferedReader br = new BufferedReader(isr); String line = null; System.out.println("<OUTPUT>"); while ( (line = br.readLine()) != null) System.out.println(line); System.out.println("</OUTPUT>"); int exitVal = proc.waitFor(); System.out.println("Process exitValue: " + exitVal); } catch (Throwable t) { t.printStackTrace(); } } }
A run of BadExecWinDir
produces:
E:\classes\com\javaworld\jpitfalls\article2>java BadExecWinDir java.io.IOException: CreateProcess: dir error=2 at java.lang.Win32Process.create(Native Method) at java.lang.Win32Process.<init>(Unknown Source) at java.lang.Runtime.execInternal(Native Method) at java.lang.Runtime.exec(Unknown Source) at java.lang.Runtime.exec(Unknown Source) at java.lang.Runtime.exec(Unknown Source) at java.lang.Runtime.exec(Unknown Source) at BadExecWinDir.main(BadExecWinDir.java:12)
As stated earlier, the error value of 2
means "file not found," which, in this case, means that the executable named dir.exe
could not be found. That's because the directory command is part of the Windows command interpreter and not a separate executable. To run the Windows command interpreter, execute either command.com
or cmd.exe
, depending on the Windows operating system you use. Listing 4.5 runs a copy of the Windows command interpreter and then executes the user-supplied command (e.g., dir
).
Listing 4.5 GoodWindowsExec.java