Thursday, October 12, 2006

Making shell calls from Java

One of our clients needs to tie an ancient DOS application into one of their web processes. Unfortunately, this application isn't really designed for use in an unattended process. It works, but occasionally (usually in error states), it will request user input. Not so spectacular if there's no user and the applications output isn't even being sent to a display. In addition, it has some serious concurrency issues and every now and again it will just fail for no discernible reason whatsoever. I was faced with three major issues here: first, how to efficiently make system calls from Java (not as trivial as it sounds!), second, how to ensure that this application isn't called by more than a certain number of threads at a time and third, how to identify those random error states and retry the operation. Just so this post doesn't drag on forever, I'll just address the first of these here, and leave the others for another day.

So, why is making shell calls from Java so difficult? All you have to do is call Runtime.getRuntime().exec() right? Well, sort of... Let me explain... round-aboutly. First off, our shell command execution needs to have some sort of timeout (to deal with those cases when the shell command hangs waiting for input). So, we need to execute the command in another thread and have a loop that waits until the process says it's done. Something that looks kind of like this perhaps (try/catch blocks and such elided for simplicity sake):

ExternalExecutionThread thread = 
new ExternalExecutionThread();
thread.setCommand(command);
long start = System.currentTimeMillis();
thread.start();
while (!thread.isFinished()) {
if (System.currentTimeMillis() - start > timeout) {
thread.interrupt();
}
}

class ExternalExecutionThread extends Thread {
private boolean finished;
private int exitCode;
private ResultListener listener;

... getters and setters

public void run() {
Process process = Runtime.getRuntime().exec(cmd);
process.waitFor();
exitCode = process.exitValue();
}
}


In addition to simply executing the command you specify, exec() provides StdErr and StdOut as streams for your monitoring convenience. A nice feature, especially when dealing with a legacy application as we are here. So, first problem. What if the command you're running doesn't output anything and then hangs? You're now stuck in a read loop, with nothing coming in. So, we need to spawn some more threads to gobble up the command output as it comes, so that our timeout will continue to work. Something more or less like this:

class StreamReadingThread extends Thread {
private OutputStream outputStream;
private InputStream inputStream;
private boolean finishedWriting;

... getters and setters

public void run() {
BufferedInputStream bis =
new BufferedInputStream(inputStream);
while (!finishedWriting) {
byte[] temp = new byte[bis.available()];
int bytesRead = bis.read(temp, 0, temp.length);
if (bytesRead == -1) {
finishedWriting = true;
} else {
outputStream.write(temp, 0, bytesRead);
}
}
}
}


Now, we modify our ExternalExecutionThread to create a couple of these StreamReadingThreads, feed them the InputStreams from the Process we started and send them on their merry way. When the process ends (or times out) we call finishWriting() on the threads and they exit gracefully. Now, I can hear the clamor, "That's a whole lot of trouble to go through to read the output! What if I don't even need the output?" Well, I can't argue with you, it is a royal pain. There's a catch here though: if you don't read the output of the command, the stream buffer will eventually fill up (assuming your command has a significant amount of output) and it will deadlock your whole thread. Gross.

At the end of the day, we've got a couple of custom classes and three new threads spawned every time we want to make a shell call. Is there a simpler way? I certainly hope so, but I've been unable to find it. I'd certainly love to be proven wrong...

No comments: