/* * Copyright 2013 John Leacox * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.openedit.util; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; /** * A {@link Closeable} wrapper for {@link Process} for running a native process. * * <p> * This wrapper provides some additional functionality around {@code Process} for some of the common pitfalls with using * {@code Process} directly. * * <ul> * * <li>It implements {@link Closeable}. The {@link #close()} method will make sure that all of the processes' streams * are closed, and if the {@code keepProcess} flag was not set, the process is destroyed via {@link Process#destroy()}.</li> * * <li>It provides the {@link #waitFor(int)} method that invokes {@link Process#waitFor()} with a timeout period. If the * process execution takes longer than the timeout, then the thread is interrupted. This method also makes sure that the * thread interrupt flag is cleared</li> * * </ul> * * <p> * Here is a basic example of using this class: * * <pre> * {@code * FinalizedProcessBuilder pb = new FinalizedProcessBuilder("myCommand", "myArg"); * FinalizedProcess process = pb.start(); * try { * int returnVal = process.waitFor(5000); * } finally { * process.close(); * }} * </pre> * * <p> * If running on Java 7, try-with-resources can be used: * * <pre> * {@code * FinalizedProcessBuilder pb = new FinalizedProcessBuilder("myCommand", "myArg"); * try (FinalizedProcess process = pb.start()) { * int returnVal = process.waitFor(5000); * }} * </pre> * * @author John Leacox * @see Process * @see FinalizedProcessBuilder * @see ProcessBuilder * */ public class FinalizedProcess implements Closeable { private final Process process; private final boolean keepProcess; private final Set<StreamGobbler> streamGobblers; FinalizedProcess(Process process, boolean keepProcess, Set<StreamGobbler> streamGobblers) { if (process == null) { throw new NullPointerException("process: null"); } this.process = process; this.keepProcess = keepProcess; this.streamGobblers = streamGobblers; } /** * Kills the subprocess. The subprocess represented by this {@code FinalizedProcess} object is forcibly terminated. */ public void destroy() { process.destroy(); } /** * Returns the exit value for the subprocess. * * @return the exit value of the subprocess represented by this {@code FinalizedProcess} object. By convention, the * value {@code 0} indicates normal termination. * @throws IllegalThreadStateException * if the subprocess represented by this {@code FinalizedProcess} object has not yet terminated */ public int exitValue() { return process.exitValue(); } /** * Returns the input stream connected to the error output of the subprocess. The stream obtains data piped from the * error output of the process represented by this {@code FinalizedProcess} object. * * <p> * If the standard error of the subprocess has been redirected using * {@link FinalizedProcessBuilder#redirectErrorStream(boolean)} then this method will return a null input * stream</a>. * * <p> * Implementation note: It is a good idea for the returned input stream to be buffered. * * @return the input stream connected to the error output of the subprocess */ public InputStream getErrorStream() { return process.getErrorStream(); } /** * Returns the input stream connected to the normal output of the subprocess. The stream obtains data piped from the * standard output of the process represented by this {@code FinalizedProcess} object. * * <p> * If the standard error of the subprocess has been redirected using * {@link FinalizedProcessBuilder#redirectErrorStream(boolean)} then the input stream returned by this method will * receive the merged standard output and the standard error of the subprocess. * * <p> * Implementation note: It is a good idea for the returned input stream to be buffered. * * @return the input stream connected to the normal output of the subprocess */ public InputStream getInputStream() { return process.getInputStream(); } /** * Returns the output stream connected to the normal input of the subprocess. Output to the stream is piped into the * standard input of the process represented by this {@code FinalizedProcess} object. * * <p> * Implementation note: It is a good idea for the returned output stream to be buffered. * * @return the output stream connected to the normal input of the subprocess */ public OutputStream getOutputStream() { return process.getOutputStream(); } /** * Causes the current thread to wait, if necessary, until the process represented by this {@code FinalizedProcess} * object has terminated. This method returns immediately if the subprocess has already terminated. If the * subprocess has not yet terminated, the calling thread will be blocked until the subprocess exits. If the * subprocess execution takes longer than the specified {@code timeoutMilliseconds}, then the blocked thread will be * interrupted. * * @param timeoutMilliseconds * time, in milliseconds, to wait on the subprocess blocking thread before timing out. (must be greater * than 0) * @return the exit value of the subprocess represented by this {@code Process} object. By convention, the value * {@code 0} indicates normal termination. * @throws IllegalArgumentException * if timeoutMilliseconds is negative or zero. * @throws InterruptedException * if the subprocess execution times out or the current thread is interrupted by another thread while it * is waiting. */ public int waitFor(long timeout) throws InterruptedException { if (timeout <= 0) { throw new IllegalArgumentException("timeoutMilliseconds: <= 0"); } TimeUnit unit = TimeUnit.MILLISECONDS; long startTime = System.nanoTime(); long rem = unit.toNanos(timeout); do { try { return exitValue(); } catch(IllegalThreadStateException ex) { if (rem > 0) Thread.sleep( Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100)); } rem = unit.toNanos(timeout) - (System.nanoTime() - startTime); } while (rem > 0); return 124; } @Override public void close() throws IOException { if (streamGobblers != null) { for (StreamGobbler gobbler : streamGobblers) { try { gobbler.close(); } catch (IOException e) { } } } if (process != null) { try { if (process.getErrorStream() != null) { process.getErrorStream().close(); } } catch (IOException e) { } try { if (process.getInputStream() != null) { process.getInputStream().close(); } } catch (IOException e) { } try { if (process.getOutputStream() != null) { process.getOutputStream().close(); } } catch (IOException e) { } if (!keepProcess) { process.destroy(); } } } // private static class InterruptTimerTask extends TimerTask { // private final Thread thread; // // public InterruptTimerTask(Thread t) { // this.thread = t; // } // // @Override // public void run() { // thread.interrupt(); // } // } public String getErrorOutputs() { StringBuffer output = new StringBuffer(); for( StreamGobbler stream : streamGobblers) { if( stream.isErrorStream() ) { output.append(stream.getOutput()); output.append("\n"); } } return output.toString(); } public String getAllOutputs() { StringBuffer output = new StringBuffer(); for( StreamGobbler stream : streamGobblers) { output.append(stream.getOutput()); output.append("\n"); } return output.toString(); } public String getStandardOutputs() { StringBuffer output = new StringBuffer(); for( StreamGobbler stream : streamGobblers) { if( !stream.isErrorStream() ) { output.append(stream.getOutput()); output.append("\n"); } } return output.toString(); } }