com.day.util.ProcessRunner Maven / Gradle / Ivy
Show all versions of aem-sdk-api Show documentation
/*************************************************************************
* ADOBE CONFIDENTIAL
* __________________
*
* Copyright 2020 Adobe
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Adobe and its suppliers, if any. The intellectual
* and technical concepts contained herein are proprietary to Adobe
* and its suppliers and are protected by all applicable intellectual
* property laws, including trade secret and copyright laws.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe.
**************************************************************************/
package com.day.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The ProcessRunner
class helps running external processes. This
* encompasses redirection of stdin, stdout and stderr as well as optionally
* waiting for the completion of the process. This implementation is based on
* the Runtime.exec(String)
method and does not yet support passing
* specific environments or decomposed comand lines.
*
* This class can be used in two ways. If you don't care about waiting for the
* processes completion and return code, you might do it like this :
*
*
* Runnable pr = new ProcessRunner("command", null, null, null);
* new Thread(pr).start();
*
*
*
* If on the other hand you want to capture all output and also keep an eye on
* the time the process takes to execute (or wait indefinitely), you might
* use the class like this :
*
*
* ProcessRunner pr = new ProcessRunner("command", in, out, err);
* pr.run(0);
* int rc = pr.getReturnCode();
*
*
*
* @version $Revision: 1.6 $
* @author fmeschbe
* @since coati
* Audience wad
*/
public class ProcessRunner implements Runnable {
/**
* Implementation note : The programm execution is implemented based on
* the Runtime.exec(String)
method. This method returns a
* Process
object, from which the streams for stdin, stdout
* and stderr can be got. These are redirected streams, that is writing
* to the OutputStream for input provides the input to the process,
* while the output from the process can be got from either the
* output stream - attached to stdout - or the error stream - attached
* to stderr.
*
*
* Capturing stdout and stderr
*
* As Java versions prior to Java 2 V 1.3 only support synchronous blocking
* IO, the streams must be polled with the help of the
* available()
method to read from the stdout and stderr
* streams. Therefor capturing stdout and stderr is implemented in a polling
* loop. Between two consecutive polls of the streams, the loop is delayed
* for {@link #POLL_OUTPUT_DELY} ms. In each poll as much bytes are
* captured as are available from the respective stream.
*
*
* <Ending the process
*
* There seems to be no other possibility to notice the end of the process
* than to constantly check the Process.exitValue()
, which
* throws an IllegalThreadStateException
as long as the process
* is running. When no exception is thrown, the process has ended and
* the polling loop can be left, as there is no more output to be expected.
*
*
* Waiting and aborting
*
* The {@link #run(long)} method implements waiting for the termination
* of the process. This method can either wait for ever or for a number
* of milliseconds for the process to end. If the time to wait passes
* without the process terminating, the process should be considered running
* to long and will be killed. To support this, the run
* methods use a {@link #lock} object and a {@link #running} flag.
*
* The waiting method {@link #run(long)} waits on the lock
* object for the specified number of milliseconds. Waiting is ended when
* either the time has passed or the process has ended. The flag is needed
* to differentiate between the timeout and the process termination. In the
* case of timeout, the process must be terminated and we have to wait for
* the real termination to settle down for getting the process return code.
*
* The {@link #run()} method notifies all waiters of the lock
* object and also clears the running flag to indicated, the process
* has really terminated. By checking this flag the {@link #run(long)}
* method can determine whether to wait for the real process termination
* or not.
*
* See also the notes in the code of {@link #run(long)} method for more
* information on this issue.
*/
/** Constant to indicate the process is running */
public static final int PROCESS_RUNNING = -1;
/** Constant to indicate the process has been aborted */
public static final int PROCESS_ABORTED = -2;
/** default log */
private static final Logger log =
LoggerFactory.getLogger(ProcessRunner.class);
/** The delay in ms between two consecutive polls for process output */
private static final int POLL_OUTPUT_DELAY = 100;
/** The cmdline to execute. */
private final String cmdLine;
/** The input data stream - where to get stdin data from */
private final InputStream stdin;
/** The output data stream - were to send stdout to */
private final OutputStream stdout;
/** The error output data stream - were to send stderr to */
private final OutputStream stderr;
/** The lock object used by the run methods to signal process termination */
private final Object lock = new Object();
/**
* set to false, after having called lock.notifyAll() but while still
* holding the monitor no lock.
*/
private boolean running = true;
/**
* The return code from running the command line or one of the
* PROCESS_*
constants.
*
* @see #PROCESS_RUNNING
* @see #PROCESS_ABORTED
*/
private int rc = PROCESS_RUNNING;
/**
* Creates a new ProcessRunner
to execute the given command
* line containing the command to execute and all relevant command
* arguments. The stdin InputStream
can be used to feed
* input data to the command executed, while the output (stdout and stderr)
* are redirected to the given OutputStream
s or the process
* defaults (System.out
and System.err
, resp.).
*
* None of the streams is closed after running the command line. This is
* the sole duty of the client of this class.
*
* @param cmdLine This commandline is given to the
* Runtime.exec(String)
contains all the command
* arguments and is split up with a StringTokenizer
.
* See the API docs on the Runtime.exec(String)
method
* for details.
* @param stdin The InputStream
containing data to be handed
* to the process as stdin. Set this to null
if the
* process should not get any input. This stream is completely
* read after the process is started and sent to the process.
* Therefor the stream should be available.
* @param stdout The OutputStream
to send the stdout output
* of the process to. If this is null
, stdout output
* is written to System.out
.
* @param stderr The OuputStream
to send the stderr output
* of the process to. If this is null
, stderr output
* is written to System.err
.
*/
public ProcessRunner(String cmdLine, InputStream stdin, OutputStream stdout,
OutputStream stderr) {
this.cmdLine = cmdLine;
this.stdin = stdin;
this.stdout = (stdout == null) ? System.out : stdout;
this.stderr = (stderr == null) ? System.err : stderr;
}
/**
* Creates a new ProcessRunner
to execute the given command
* line containing the command to execute and all relevant command
* arguments with no redirection of stdin, stdout and stderr.
*
* @param cmdLine This commandline is given to the
* Runtime.exec(String)
contains all the command
* arguments and is split up with a StringTokenizer
.
* See the API docs on the Runtime.exec(String)
method
* for details.
*/
public ProcessRunner(String cmdLine) {
this(cmdLine, null, null, null);
}
/**
* Executes the command line and waits for the completion of the prcoess.
* The process runs in its own thread which is marked as daemon thread. That
* is as soon as the Java VM is about to end, the process will also forcibly
* stopped and does not run to completion.
*
* @param waitTime The number of milliseconds to wait for the completion of
* the process. If the time has ellapsed before the process has
* terminated, the process will be forcibly terminated. If this
* value is negative, the process runs completely detached, while
* a value of zero indicates to wait indeterminate for the
* completion of the process.
*/
public void run(long waitTime) {
Thread t = new Thread(this);
t.setDaemon(true);
t.start();
if (waitTime >= 0) {
synchronized(lock) {
while (running) {
try {
// wait for the waitTime (forever if waitTime==0)
lock.wait(waitTime);
/**
* Two possibilities here : timeout or notifyall.
*
* (1) after lock.notifyAll(), the process has ended and
* running == false. all is well.
*
* (2) after timeout, the process may have ended or not.
* the problem is, that when waking up and contending
* for the lock monitor, the process might come first
* and lock.notifyAll(). if this happens, that signal
* is lost but i'm sure that running==false, as i only
* get the monitor, when the process releases it.
* if i win - that is running==true - i know, that i
* can catch a lock.notifyAll() later.
*
* that is : if i get to run again, i do not know
* whether i was waken up by a timeout or a notifyAll()
* and i do not directly know whether the notifyAll()
* has been sent. thanks to the running flag, i can be
* made sure of this : if i get to run, then either the
* notifyAll() has been sent (whether or not i directly
* caught it) or will not be sent as long as i do not
* release the monitor.
*/
// no notifyAll, yet
if (running) {
// send an interrupt to be sure
t.interrupt();
// force no timeout at next wait, notifyAll() comes
waitTime = 0;
}
} catch (InterruptedException ie) {
// ignore
}
}
}
}
}
/**
* Returns the return code from running the command line. As long as the
* command is running, the method returns {@link #PROCESS_RUNNING}. After
* the process has terminated, the method returns the return code from the
* process or {@link #PROCESS_ABORTED} if the command has been terminated
* due to a timeout.
*
* @return The return code from the process or either
* PROCESS_RUNNING
or PROCESS_ABORTED
.
*/
public int getReturnCode() {
return rc;
}
//---------- Runnable interface --------------------------------------------
/**
* Runs the process sending the input data and capturing output data.
*
* Do not directly call this method. Instead either use
* new Thread(new ProcessRunner(cmd)).start()
or use the
* {@link #run(long)} method.
*/
public void run() {
// need to close them in the finally block
InputStream procOut = null;
InputStream procErr = null;
Process p = null;
try {
// start the process and get the output streams
p = Runtime.getRuntime().exec(cmdLine);
procOut = p.getInputStream();
procErr = p.getErrorStream();
// Send input data to the process
writeStdIn(p, stdin);
// now try and catch the output of the process
while (p != null) {
// let the process some time to do some work
Thread.sleep(POLL_OUTPUT_DELAY);
// read stdout and stderr
spoolAvailable(procOut, stdout);
spoolAvailable(procErr, stderr);
try {
// IllegalThreadStateException is thrown if the process
// is still running, else we get the exit code and
// end waiting
rc = p.exitValue();
// terminate the loop
p = null;
} catch (IllegalThreadStateException e) {}
}
log.info("run: Process ended with rc={}", new Integer(rc));
} catch (IOException ioe) {
log.info("run: IO problem during execution: {}", ioe.toString());
} catch (InterruptedException ie) {
// we were interrupted for some reason, destroy the proces before
// continuing.
if (p != null) {
p.destroy();
try {
rc = p.exitValue();
} catch (IllegalThreadStateException itse) {
log.info("run: Exit value of destroyed process unavailable");
}
p = null;
}
} finally {
tryClose(procOut);
tryClose(procErr);
// inform waiters on the termination
synchronized (lock){
lock.notifyAll();
running = false;
}
}
}
//---------- internal ------------------------------------------------------
/**
* Sends the input data from the client to the process. The method copies
* all data available from the InputStream
to the process,
* that is, all data up to an error or the end of file is copied.
*
* The stream to write the data to is retrieved from the process and closed
* at the end regardless of whether an error occurred sending the data.
*
* @param proc The Process
to send the data to
* @param stdin The InputStream
to read the data from.
*
* @throws IOException if reading or writing the data throws an error.
*/
private void writeStdIn(Process proc, InputStream stdin) throws IOException {
if (stdin != null) {
final int bufLen = 1024;
final byte[] b = new byte[bufLen];
OutputStream procIn = null;
try {
procIn = proc.getOutputStream();
int numRead;
while ( (numRead = stdin.read(b)) > 0 ) {
procIn.write(b, 0, numRead);
}
} finally {
tryClose(procIn);
}
}
}
/**
* Spools available input data from the source InputStream
to
* the sink OutputStream
.
*
* @param source The InputStream
providing data.
* @param sink The OutputStream
getting the data.
*
* @throws IOException if reading or writing the data has problems.
*/
private void spoolAvailable(InputStream source, OutputStream sink)
throws IOException {
final int bufLen = 1024;
final byte[] buf = new byte[bufLen];
for (int avail=source.available(); avail > 0; ) {
int num = (avail > bufLen) ? bufLen : avail;
num = source.read(buf, 0, num);
// assume available is correct ;-)
sink.write(buf, 0, num);
avail -= num;
}
}
/**
* Closes the OutputStream
if not null
and eats
* (ignores) any IOException
occurrring.
*
* @param out The OutputStream
to close ignoring any
* IOException
occurring.
*/
private void tryClose(OutputStream out) {
if (out != null) {
try {
out.close();
} catch (IOException ioe) {}
}
}
/**
* Closes the InputStream
if not null
and eats
* (ignores) any IOException
occurrring.
*
* @param in The InputStream
to close ignoring any
* IOException
occurring.
*/
private void tryClose(InputStream in) {
if (in != null) {
try {
in.close();
} catch (IOException ioe) {}
}
}
}