import java.util.*; import java.io.*; class StreamGobbler extends Thread { InputStream is; String type; StreamGobbler(InputStream is, String type) { this.is = is; this.type = type; } public void run() { try { InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); String line=null; while ( (line = br.readLine()) != null) System.out.println(type + ">" + line); } catch (IOException ioe) { ioe.printStackTrace(); } } } public class GoodWindowsExec { public static void main(String args[]) { if (args.length < 1) { System.out.println("USAGE: java GoodWindowsExec <cmd>"); System.exit(1); } try { String osName = System.getProperty("os.name" ); String[] cmd = new String[3]; if( osName.equals( "Windows NT" ) ) { cmd[0] = "cmd.exe" ; cmd[1] = "/C" ; cmd[2] = args[0]; } else if( osName.equals( "Windows 95" ) ) { cmd[0] = "command.com" ; cmd[1] = "/C" ; cmd[2] = args[0]; } Runtime rt = Runtime.getRuntime(); System.out.println("Execing " + cmd[0] + " " + cmd[1] + " " + cmd[2]); Process proc = rt.exec(cmd); // any error message? StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERROR"); // any output? StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUTPUT"); // kick them off errorGobbler.start(); outputGobbler.start(); // any error??? int exitVal = proc.waitFor(); System.out.println("ExitValue: " + exitVal); } catch (Throwable t) { t.printStackTrace(); } } }
Running GoodWindowsExec
with the dir
command generates:
E:\classes\com\javaworld\jpitfalls\article2>java GoodWindowsExec "dir *.java" Execing cmd.exe /C dir *.java OUTPUT> Volume in drive E has no label. OUTPUT> Volume Serial Number is 5C5F-0CC9 OUTPUT> OUTPUT> Directory of E:\classes\com\javaworld\jpitfalls\article2 OUTPUT> OUTPUT>10/23/00 09:01p 805 BadExecBrowser.java OUTPUT>10/22/00 09:35a 770 BadExecBrowser1.java OUTPUT>10/24/00 08:45p 488 BadExecJavac.java OUTPUT>10/24/00 08:46p 519 BadExecJavac2.java OUTPUT>10/24/00 09:13p 930 BadExecWinDir.java OUTPUT>10/22/00 09:21a 2,282 BadURLPost.java OUTPUT>10/22/00 09:20a 2,273 BadURLPost1.java ... (some output omitted for brevity) OUTPUT>10/12/00 09:29p 151 SuperFrame.java OUTPUT>10/24/00 09:23p 1,814 TestExec.java OUTPUT>10/09/00 05:47p 23,543 TestStringReplace.java OUTPUT>10/12/00 08:55p 228 TopLevel.java OUTPUT> 22 File(s) 46,661 bytes OUTPUT> 19,678,420,992 bytes free ExitValue: 0
Running GoodWindowsExec
with any associated document type will launch the application associated with that document type. For example, to launch Microsoft Word to display a Word document (i.e., one with a .doc
extension), type:
>java GoodWindowsExec "yourdoc.doc"
Notice that GoodWindowsExec
uses the os.name
system property to determine which Windows operating system you are running -- and thus determine the appropriate command interpreter. After executing the command interpreter, handle the standard error and standard input streams with the StreamGobbler
class. StreamGobbler
empties any stream passed into it in a separate thread. The class uses a simple String
type to denote the stream it empties when it prints the line just read to the console.
Thus, to avoid the third pitfall related to Runtime.exec()
, do not assume that a command is an executable program; know whether you are executing a standalone executable or an interpreted command. At the end of this section, I will demonstrate a simple command-line tool that will help you with that analysis.
It is important to note that the method used to obtain a process's output stream is called getInputStream()
. The thing to remember is that the API sees things from the perspective of the Java program and not the external process. Therefore, the external program's output is the Java program's input. And that logic carries over to the external program's input stream, which is an output stream to the Java program.
Runtime.exec() is not a command line
One final pitfall to cover with Runtime.exec()
is mistakenly assuming that exec()
accepts any String
that your command line (or shell) accepts. Runtime.exec()
is much more limited and not cross-platform. This pitfall is caused by users attempting to use the exec()
method to accept a single String
as a command line would. The confusion may be due to the fact that command
is the parameter name for the exec()
method. Thus, the programmer incorrectly associates the parameter command with anything that he or she can type on a command line, instead of associating it with a single program and its arguments. In listing 4.6 below, a user tries to execute a command and redirect its output in one call to exec()
:
Listing 4.6 BadWinRedirect.java
import java.util.*; import java.io.*; // StreamGobbler omitted for brevity public class BadWinRedirect { public static void main(String args[]) { try { Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("java jecho 'Hello World' > test.txt"); // any error message? StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERROR"); // any output? StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUTPUT"); // kick them off errorGobbler.start(); outputGobbler.start(); // any error??? int exitVal = proc.waitFor(); System.out.println("ExitValue: " + exitVal); } catch (Throwable t) { t.printStackTrace(); } } }
Running BadWinRedirect
produces:
E:\classes\com\javaworld\jpitfalls\article2>java BadWinRedirect OUTPUT>'Hello World' > test.txt ExitValue: 0
The program BadWinRedirect
attempted to redirect the output of an echo program's simple Java version into the file test.txt
. However, we find that the file test.txt
does not exist. The jecho
program simply takes its command-line arguments and writes them to the standard output stream. (You will find the source for jecho
in the source code available for download in Resources.) In Listing 4.6, the user assumed that you could redirect standard output into a file just as you could on a DOS command line. Nevertheless, you do not redirect the output through this approach. The incorrect assumption here is that the exec()
method acts like a shell interpreter; it does not. Instead, exec()
executes a single executable (a program or script). If you want to process the stream to either redirect it or pipe it into another program, you must do so programmatically, using the java.io
package. Listing 4.7 properly redirects the standard output stream of the jecho
process into a file.
Listing 4.7 GoodWinRedirect.java
import java.util.*; import java.io.*; class StreamGobbler extends Thread { InputStream is; String type; OutputStream os; StreamGobbler(InputStream is, String type) { this(is, type, null); } StreamGobbler(InputStream is, String type, OutputStream redirect) { this.is = is; this.type = type; this.os = redirect; } public void run() { try { PrintWriter pw = null; if (os != null) pw = new PrintWriter(os); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); String line=null; while ( (line = br.readLine()) != null) { if (pw != null) pw.println(line); System.out.println(type + ">" + line); } if (pw != null) pw.flush(); } catch (IOException ioe) { ioe.printStackTrace(); } } } public class GoodWinRedirect { public static void main(String args[]) { if (args.length < 1) { System.out.println("USAGE java GoodWinRedirect <outputfile>"); System.exit(1); } try { FileOutputStream fos = new FileOutputStream(args[0]); Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("java jecho 'Hello World'"); // any error message? StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERROR"); // any output? StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUTPUT", fos); // kick them off errorGobbler.start(); outputGobbler.start(); // any error??? int exitVal = proc.waitFor(); System.out.println("ExitValue: " + exitVal); fos.flush(); fos.close(); } catch (Throwable t) { t.printStackTrace(); } } }
Running GoodWinRedirect
produces:
E:\classes\com\javaworld\jpitfalls\article2>java GoodWinRedirect test.txt OUTPUT>'Hello World' ExitValue: 0
After running GoodWinRedirect
, test.txt
does exist. The solution to the pitfall was to simply control the redirection by handling the external process's standard output stream separately from the Runtime.exec()
method. We create a separate OutputStream
, read in the filename to which we redirect the output, open the file, and write the output that we receive from the spawned process's standard output to the file. Listing 4.7 completes that task by adding a new constructor to our StreamGobbler
class. The new constructor takes three arguments: the input stream to gobble, the type String
that labels the stream we are gobbling, and the output stream to which we redirect the input. This new version of StreamGobbler
does not break any of the code in which it was previously used, as we have not changed the existing public API -- we only extended it.
Since the argument to Runtime.exec()
is dependent on the operating system, the proper commands to use will vary from one OS to another. So, before finalizing arguments to Runtime.exec()
and writing the code, quickly test the arguments. Listing 4.8 is a simple command-line utility that allows you to do just that.
Here's a useful exercise: try to modify TestExec
to redirect the standard input or standard output to a file. When executing the javac
compiler on Windows 95 or Windows 98, that would solve the problem of error messages scrolling off the top of the limited command-line buffer.
Listing 4.8 TestExec.java
import java.util.*; import java.io.*; // class StreamGobbler omitted for brevity public class TestExec { public static void main(String args[]) { if (args.length < 1) { System.out.println("USAGE: java TestExec \"cmd\""); System.exit(1); } try { String cmd = args[0]; Runtime rt = Runtime.getRuntime(); Process proc = rt.exec(cmd); // any error message? StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERR"); // any output? StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUT"); // kick them off errorGobbler.start(); outputGobbler.start(); // any error??? int exitVal = proc.waitFor(); System.out.println("ExitValue: " + exitVal); } catch (Throwable t) { t.printStackTrace(); } } }
Running TestExec
to launch the Netscape browser and load the Java help documentation produces:
E:\classes\com\javaworld\jpitfalls\article2>java TestExec "e:\java\docs\index.html" java.io.IOException: CreateProcess: e:\java\docs\index.html error=193 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 TestExec.main(TestExec.java:45)
Our first test failed with an error of 193
. The Win32 error for value 193 is "not a valid Win32 application." This error tells us that no path to an associated application (e.g., Netscape) exists, and that the process cannot run an HTML file without an associated application.
Therefore, we try the test again, this time giving it a full path to Netscape. (Alternately, we could add Netscape to our PATH
environment variable.) A second run of TestExec
produces:
E:\classes\com\javaworld\jpitfalls\article2>java TestExec "e:\program files\netscape\program\netscape.exe e:\java\docs\index.html" ExitValue: 0
This worked! The Netscape browser launches, and it then loads the Java help documentation.
One additional improvement to TestExec
would include a command-line switch to accept input from standard input. You would then use the Process.getOutputStream()
method to pass the input to the spawned external program.
To sum up, follow these rules of thumb to avoid the pitfalls in Runtime.exec()
:
- You cannot obtain an exit status from an external process until it has exited
- You must immediately handle the input, output, and error streams from your spawned external process
- You must use
Runtime.exec()
to execute programs - You cannot use
Runtime.exec()
like a command line
Correction to Pitfall 3
In the discussion of Pitfall 3 ("Don't mix floats and doubles when generating text or XML messages") in my last column, I incorrectly stated that the different string representation of a decimal number after casting it from a float to a double was a bug. While this is a pitfall, its cause is not a bug, but the fact that the decimal numbers in question -- 100.28 and 91.09 -- do not represent precisely in binary. I'd like to thank Thomas Okken and the others who straightened me out. If you enjoy discussing the finer points of numerical methods, you can email Thomas.