Running Windows CMD and Batch Scripts from Java
A couple of weeks ago I hit a problem at work that would require calling an external program from Java. Essentially I needed to run a Windows batch script from within a program, so I decided I would make a boilerplate for Java Runtimes and add it to the company development library such that nobody else would have to boilerplate it in the future. This ended up being a ridiculously difficult challenge due to Java's assumptions on how programs will run and CMD's weird edge cases. But before I can begin complaining about CMD, I should explain the Java side.
Runtimes and Processes in Java
Just like in Java how ExecutorServices take Callables and build Futures (which is coming soon in a gooey, sticky post), Runtimes take commands and build Processes. A simple example of what this looks like:
// Make a Runtime
Runtime runtime = Runtime.getRuntime();
// Pass the Runtime a command and get a Process representing that instance
Process process = runtime.exec("cmd /C echo hello world && exit 0");
// Get exit code of command and print
Integer exitCode = new Integer(process.waitFor());
System.out.println(exitCode);
As you can see, you simply build a command, pass it to the Runtime, and get a Process back to monitor and query for results. However, there's quite a bit this example ignores. Let's start for the immediately funky stuff: why from Windows do I explicitly have to call "cmd /C" before the actual command? Well, Java thinks you want to run a program, not a script. So you have to explicitly tell it you want to run a script using the CMD program. Phew, OK. And why do I wrap the original command with a seemingly needless "exit 0"? Well, running "cmd /C" on its own (which is meant to run only one command and exit the terminal) will never send an exit signal to Java. So you need to force it to send an exit code after running your commands, otherwise CMD won't explicitly close and Java will hang waiting for an exit code. This is just the tip of the iceberg, by the way. This will get a lot, lot worse very, very fast.
Getting output from Processes
Anyways, what if I needed some output from the command? This is where another ugly rule of Java Processes pops up its boil and wart infested mug; if any of a Process' streams are full and waiting to be read all other streams will block waiting for that one to complete. This means you cannot safely read the streams of a default Process sequentially in a loop; you must read them asynchronously from Threads or one is liable to fill and cause lock up. There is an exception for this with ProcessBuilders that I'll be going over a couple sections down, by the way. Look at this code and understand it in terms of the problem; don't start emulating it with your own solution, because there is a much better approach.
// Make a Runtime
Runtime runtime = Runtime.getRuntime();
// Pass the Runtime a command and get a Process representing that instance
Process process = runtime.exec("cmd /C echo hello world && exit 0");
// Make some stream readers for the process
BufferedReader inputStream = new BufferedReader(new InputStreamReader (process.getInputStream));
BufferedReader errorStream = new BufferedReader(new InputStreamReader (process.getErrorStream));
String inputLine;
String errorLine;
// Output everything from the stream
while((inputLine = inputStream.readLine()) != null && (errorLine = errorStream.readLine()) != null){
System.out.println(inputLine);
}
// Get exit code of command and print
Integer exitCode = new Integer(process.waitFor());
System.out.println(exitCode);
Why won't this work? Well, what if in your loop the error stream fills before you've read the input stream (which in this case is really output; I know, it's weird)? Then you'd be blocked on reading the input stream because the error stream had filled first. The opposite condition is also possible; if the input stream is full but you're blocked on reading the error stream. How do we combat this? Well, there's no synchronous solution using a Runtime and Process alone. Enter the ProcessBuilder stage left! It has an option to redirect the error stream to the input stream, allowing you to only watch one stream on repeat with confidence that no other stream will fill and force you into a dead lock. Unfortunately, ProcessBuilders require an ArrayList representing the command to be run rather than a simple String as shown above.
Building a ProcessBuilder Windows CMD Command
Before we can run a command, we first need to build it! This all starts with a String ArrayList (I know, I know, this seems weird, stay with me for a minute). We build the ArrayList from components of a command; we pass the program name and arguments as separate entries to the list. We need to do this to use the ProcessBuilder class. However, because we're working with Windows CMD, this proves a process in insanity.
//Build command in ArrayList<String>
//There are a lot of interesting reasons it has to be done EXACTLY this way...
command.add("cmd");
//First off the bat, we need to tell it to run only this command on its own
//Otherwise it will spawn a new terminal or go to interactive mode
command.add("/C");
//Next we delicately explain to cmd what the command is, wrapping it in an extra set of double quotes
//Without the extra set of double quotes everything breaks because the syntax doesn't match CMD's expectations
command.add ("\"" + pathToBatchScript);
//Oh, yeah. CMD won't exit by default, even with /C, so we need to force it to by adding an 'exit'
command.add("&");
command.add("exit");
//And finally, to freaking know the final exit code, we need to send that as a parameter to 'exit'
command.add("%errorlevel%\"");
//Yup, you need to do all of that junk in that order exactly to run a batch script while maintaining its output and exit codes
//Took me about 6 hours to get this working
//I don't think its perfect at all
So, this is how you build a CMD command to run a batch script in Java. You'll notice all the same features as above. You start by telling Windows "I want to run 'cmd'", and then play a dangerous game of balancing arbitrary CMD syntax around the path of the batch script provided. You have to enclose the CMD command with a wrapping "exit %errorlevel%", because otherwise CMD won't report to Java that it is complete when it exits. You need to start the command with a "/C" such that CMD won't stay open after running your command. It's a vulgar, disgusting, terrible approach. But it's the only way I can find that works. The most disturbing part? When I started working with Jenkins, I noticed they do the exact same thing. They wrap whatever commands you pass with "/C" and "&& EXIT %ERRORLEVEL%" to achieve the same effect.
But how do you take this and run it? Well, you pass it to a ProcessBuilder constructor when instantiating, of course! I'll be going over how to use the ProcessBuilder and all of it's own issues in the next post! Stay tuned!