package org.cdlib.xtf.util; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.Timer; import java.util.TimerTask; /** * Copyright (c) 2009, Regents of the University of California * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the University of California nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ public class ProcessRunner { /** * Run the external process, applying a timeout if specified, feeding it * input on stdin and gathering the results from stdout. If a non-zero * exit status is returned, we throw an exception containing the output * string from stderr. The input and output are encoded using the * system-default charset. * * @throws IOException If something goes wrong starting the process. * @throws InterruptedException If process exceeds the given timeout. * @throws CommandFailedException If the process exits with non-zero status. */ public static String runAndGrab(String[] argArray, String input, int timeout) throws InterruptedException, CommandFailedException, IOException { Charset charset = Charset.defaultCharset(); ByteBuffer inBuf = charset.encode(input); inBuf.compact(); byte[] inBytes = inBuf.array(); byte[] outBytes = runAndGrab(argArray, inBytes, timeout); CharBuffer outBuf = charset.decode(ByteBuffer.wrap(outBytes)); String output = outBuf.toString(); return output; } /** * Run the external process, applying a timeout if specified, feeding it * input on stdin and gathering the results from stdout. If a non-zero * exit status is returned, we throw an exception containing the output * string from stderr. * * @throws IOException If something goes wrong starting the process. * @throws InterruptedException If process exceeds the given timeout. * @throws CommandFailedException If the process exits with non-zero status. */ public static byte[] runAndGrab(String[] argArray, byte[] inputBytes, int timeout) throws InterruptedException, CommandFailedException, IOException { // Get ready to go Process process = null; InputStream stdout = null; OutputGrabber stdoutGrabber = null; InputStream stderr = null; OutputGrabber stderrGrabber = null; OutputStream stdin = null; InputStuffer stdinStuffer = null; Timer timer = null; Interrupter interrupter = null; boolean exception = false; try { // Fire up the process. process = Runtime.getRuntime().exec(argArray); // Stuff input into it (even if that input is nothing). stdin = process.getOutputStream(); stdinStuffer = new InputStuffer(stdin, inputBytes); stdinStuffer.start(); // Grab all the output stdout = process.getInputStream(); stdoutGrabber = new OutputGrabber(stdout); stderr = process.getErrorStream(); stderrGrabber = new OutputGrabber(stderr); stdoutGrabber.start(); stderrGrabber.start(); // Set a timer so we can stop the process if it exceeds the timeout. if (timeout > 0) { interrupter = new Interrupter(Thread.currentThread()); timer = new Timer(); timer.schedule(interrupter, timeout); } // Wait for the process to finish. process.waitFor(); } // try catch (IOException e) { exception = true; throw e; } catch (InterruptedException e) { exception = true; throw e; } finally { if (interrupter != null) { synchronized (interrupter) { timer.cancel(); // avoid further interruptions interrupter.mainThread = null; // make sure it can't get to us Thread.interrupted(); // clear out the interrupted flag. } } if (exception) { if (stdinStuffer != null) stdinStuffer.interrupt(); if (stdoutGrabber != null) stdoutGrabber.interrupt(); if (stderrGrabber != null) stderrGrabber.interrupt(); if (process != null) process.destroy(); if (stdin != null) try { stdin.close(); } catch (IOException e2) { /*ignore*/ } if (stdout != null) try { stdout.close(); } catch (IOException e2) { /*ignore*/ } if (stderr != null) try { stderr.close(); } catch (IOException e2) { /*ignore*/ } } } // finally // Wait for the stuffer and grabbers to finish their work. try { if (stdinStuffer != null) { while (true) { synchronized (stdinStuffer) { if (stdinStuffer.done) break; stdinStuffer.wait(); } } } while (true) { synchronized (stdoutGrabber) { if (stdoutGrabber.done) break; stdoutGrabber.wait(); } } while (true) { synchronized (stderrGrabber) { if (stderrGrabber.done) break; stderrGrabber.wait(); } } } catch (InterruptedException e) { assert false : "should not be interrupted at this stage"; } // Make sure all the streams are closed (to avoid leaking.) if (stdin != null) try { stdin.close(); } catch (IOException e) { /*ignore*/ } if (stdout != null) try { stdout.close(); } catch (IOException e) { /*ignore*/ } if (stderr != null) try { stderr.close(); } catch (IOException e) { /*ignore*/ } // If we got a non-zero exit status, and something came out on stderr, // then throw an exception. // if (process.exitValue() != 0) { String errStr = new String(stderrGrabber.outBytes); throw new CommandFailedException( "External command '" + argArray[0] + "' exited with status " + process.exitValue() + ". Output from stderr:\n" + errStr); } // Return the results from stdout return stdoutGrabber.outBytes; } /** Used to interrupt the main thread if a timeout occurs */ private static class Interrupter extends TimerTask { public Thread mainThread; public Interrupter(Thread mainThread) { this.mainThread = mainThread; } public synchronized void run() { if (mainThread != null) mainThread.interrupt(); } } // class Interrupter /** * Class to stuff input into the process's input stream (an OutputStream to * us). */ private static class InputStuffer extends Thread { private OutputStream outStream; private byte[] bytes; public Throwable error; public boolean done = false; public InputStuffer(OutputStream stream, byte[] bytes) throws UnsupportedEncodingException { this.outStream = stream; this.bytes = bytes; } public void run() { try { // Write all the data. outStream.write(bytes); // Inform the process that this is the end now. outStream.close(); } // try catch (IOException e) { error = e; } finally { synchronized (this) { done = true; notifyAll(); } } } // run() } // class InputStuffer /** * Class to accumulate the output from a process's output stream (which is * an InputStream to us), and turn it into a string. */ private static class OutputGrabber extends Thread { private InputStream inStream; private ByteArrayOutputStream buffer = new ByteArrayOutputStream(100); public byte[] outBytes = new byte[0]; public Throwable error; public boolean done = false; public OutputGrabber(InputStream stream) { this.inStream = stream; } public void run() { // Read data until it's exhausted (we will be interrupted when it's time // to stop.) // try { byte[] tmp = new byte[4096]; while (true) { // Read some stuff. int got = inStream.read(tmp); if (got < 0) break; // Save it up. buffer.write(tmp, 0, got); } // Get a byte array that the caller can process. outBytes = buffer.toByteArray(); } // try catch (IOException e) { error = e; } finally { synchronized (this) { done = true; notifyAll(); } } } // run() } // class OutputGrabber /** * Exception thrown if an external command ends with a non-zero exit * status. */ public static class CommandFailedException extends Exception { public CommandFailedException() { super(); } public CommandFailedException(String s) { super(s); } } }