+ * This provides similar apis than {@link ExternalProcess}. But has been adapted so that creation + * of the instance starts the process asynchronously. Additional apis are also provided to + * wait for the process and/or terminate it forcibly when no longer needed. + * + * @author Kris De Volder + */ +public class AsyncProcess { + + //TODO: much of this code copied from ExternalProcess. It should be relatively easy + // to make 'ExternalProcess inherit from this one and just add some additional 'waitFor' call + // in its 'init' method. + + /** + * A thread that keeps reading input from a Stream until the end is reached + * or there's some error reading the Stream. + */ + public static class StreamGobler extends Thread { + + private final OutputStream echo; + private InputStream toRead; //Stream to read. This is nulled after all input has been consumed. + + /** + * Creates a StreamGobler that reads input from an input stream + * and buffers up all input it has read for later retrieval via + * the getOut() method. + */ + public StreamGobler(InputStream toRead) { + this(toRead, new ByteArrayOutputStream()); + } + + /** + * Creates a StreamGobler that reads input from an input stream + * and writes it out to an outputstream. + */ + public StreamGobler(InputStream toRead, OutputStream forwardTo) { + this.toRead = toRead; + this.echo = forwardTo; + start(); + } + + @Override + public void run() { + byte[] buf = new byte[256]; + while (toRead!=null) { + try { + int i = toRead.read(buf); + if (i==-1) { + //EOF + toRead = null; //Done! + } else { + append(buf, i); + } + } catch (IOException e) { + toRead = null; + ByteArrayOutputStream errMsg = new ByteArrayOutputStream(); + e.printStackTrace(new PrintStream(errMsg)); + append(errMsg.toByteArray()); + } + } + } + + private void append(byte[] buf) { + append(buf, buf.length); + } + + private void append(byte[] buf, int len) { + if (echo!=null) { + try { + echo.write(buf, 0, len); + } catch (IOException e) { + } + } + } + + public String getContents() throws InterruptedException { + try { + this.join(); + if (echo instanceof ByteArrayOutputStream) { + return ((ByteArrayOutputStream)echo).toString(); + } else { + return null; + } + } finally { + toRead = null; + } + } + } + + private Process process; + private StreamGobler err; // Standard error is to be read from here + private StreamGobler out; // Standard out is to be read from here + private int exitValue = -9999; + private ExternalCommand cmd; + + /** + * Creates an external process and waits for it to terminate. The output and error streams + * will be read and forwarded to System.out and System.err + */ + public AsyncProcess(File workingDir, ExternalCommand cmd) throws IOException { + this(workingDir, cmd, false); + } + + private void init(File workingDir, ExternalCommand cmd, + OutputStream outStream, OutputStream errStream + ) throws IOException { + this.cmd = cmd; + ProcessBuilder processBuilder = new ProcessBuilder(cmd.getProgramAndArgs()); + processBuilder.directory(workingDir); + cmd.configure(processBuilder); + process = processBuilder.start(); + err = new StreamGobler(process.getErrorStream(), errStream); + out = new StreamGobler(process.getInputStream(), outStream); + } + + public AsyncProcess(File workingDir, ExternalCommand cmd, boolean captureStreams) throws IOException { + if (captureStreams) { + init(workingDir, cmd, new ByteArrayOutputStream(), new ByteArrayOutputStream()); + } else { + init(workingDir, cmd, System.out, System.err); + } + } + + public String getOut() throws InterruptedException { + return out.getContents(); + } + + public String getErr() throws InterruptedException { + return err.getContents(); + } + + /** + * Wait for the process to exit. If a timeout is specified and the process + * does not terminate within that time, the process is destroyed forcibly. + *
+ * If timeout is not specified (i.e. null) then this waits for as long as necessary + * (which is potentially forever if the process does not terminate). + */ + public int waitFor(Duration timeout) throws InterruptedException, TimeoutException { + try { + if (timeout==null) { + exitValue = process.waitFor(); + } else { + if (process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + exitValue = process.exitValue(); + } else { + process.destroy(); + exitValue = 999; //Set some non-0 value as that is what some callers might use to determine 'failure' occurred. + throw new TimeoutException("Command timed out: "+this); + } + } + return exitValue; + } finally { + try { + //Try destroying unresponsive processes no matter what. + process.destroyForcibly(); + } catch (Exception e) { + //ignore + } + } + } + + @Override + public String toString() { + StringBuffer result = new StringBuffer(); + try { + process.exitValue(); + result.append(">>>> ExternalProcess: "); + result.append(cmd+"\n"); + result.append("exitValue = "+exitValue+"\n"); + String strOut = getOut(); + if (strOut!=null) { + result.append("------- System.out -------\n"); + result.append(strOut); + } + String strErr = getErr(); + if (strErr!=null) { + result.append("------- System.err -------\n"); + result.append(strErr); + } + result.append("<<<< ExternalProcess"); + return result.toString(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return result.toString(); + } catch (IllegalThreadStateException e) { + return "ExternalProcess(RUNNING, "+cmd+")"; + } + } + + public int getExitValue() { + return exitValue; + } + + public void kill() { + try { + waitFor(Duration.ofSeconds(0)); + } catch (Exception e) { + } + } +}