package com.bc.ceres.core;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* An observer that notifies its {@link ProcessObserver.Handler handlers} about lines of characters that have been written
* by a process to both {@code stdout} and {@code stderr} output streams.
* <p/>
* <pre>
* TODO
*
* Develop External Process Invocation API (EPIA)
*
* - idea: use velocity to generate input files + command-line from current context
* - address that executables might have different extensions (and paths) on different OS (.exe, .bat, .sh)
*
* Process Descriptor
*
* - name
* - description
*
* - n input descriptors (descriptor: name + type + description)
* - n output descriptors (descriptor: name + type + description)
* - n parameter descriptors (descriptor: name + type + description + attributes)
* - 1 command-line velocity template
* - 1 relative working directory template
* - n environment variables templates
* - n velocity file templates
* - n static files
* - n static archives to unpack
*
* Process Invocation
*
* - prepare context:
* set environment variables
* set inputs, outputs, parameters
*
* </pre>
*
* @author Norman Fomferra
*/
public class ProcessObserver {
/**
* The observation mode.
*/
public enum Mode {
/**
* {@link ProcessObserver#start()} blocks until the process ends.
*/
BLOCKING,
/**
* {@link ProcessObserver#start()} returns immediately.
*/
NON_BLOCKING,
}
private static final String MAIN = "main";
private static final int STDOUT = 0;
private static final int STDERR = 1;
private final Process process;
private String name;
private int pollPeriod;
private ProgressMonitor progressMonitor;
private Handler handler;
private Mode mode;
private ObservedProcessImpl observedProcess;
/**
* Constructor.
*
* @param process The process to be observed
*/
public ProcessObserver(final Process process) {
this.process = process;
this.name = "process";
this.pollPeriod = 500;
this.progressMonitor = new NullProgressMonitor();
this.handler = new DefaultHandler();
this.mode = Mode.BLOCKING;
}
/**
* @return A name that represents the process.
*/
public String getName() {
return name;
}
/**
* Default is "process".
*
* @param name A name that represents the process.
* @return this
*/
public ProcessObserver setName(String name) {
Assert.notNull(name, "name");
this.name = name;
return this;
}
/**
* @return A handler.
*/
public Handler getHandler() {
return handler;
}
/**
* Default-handler prints to stdout / stderr.
*
* @param handler A handler.
* @return this
*/
public ProcessObserver setHandler(Handler handler) {
Assert.notNull(handler, "handler");
this.handler = handler;
return this;
}
/**
* @return A progress monitor.
*/
public ProgressMonitor getProgressMonitor() {
return progressMonitor;
}
/**
* Default does nothing.
*
* @param progressMonitor A progress monitor.
* @return this
*/
public ProcessObserver setProgressMonitor(ProgressMonitor progressMonitor) {
Assert.notNull(progressMonitor, "progressMonitor");
this.progressMonitor = progressMonitor;
return this;
}
/**
* @return The observation mode.
*/
public Mode getMode() {
return mode;
}
/**
* Default is {@link Mode#BLOCKING}.
*
* @param mode The observation mode.
* @return this
*/
public ProcessObserver setMode(Mode mode) {
Assert.notNull(mode, "mode");
this.mode = mode;
return this;
}
/**
* @return Time in milliseconds between successive process status queries.
*/
public int getPollPeriod() {
return pollPeriod;
}
/**
* Default is 500 milliseconds.
*
* @param pollPeriod Time in milliseconds between successive process status queries.
* @return this
*/
public ProcessObserver setPollPeriod(int pollPeriod) {
Assert.notNull(pollPeriod, "pollPeriod");
this.pollPeriod = pollPeriod;
return this;
}
/**
* Starts observing the given process.
*/
public ObservedProcess start() {
if (observedProcess != null) {
throw new IllegalStateException("process already observed.");
}
observedProcess = new ObservedProcessImpl();
observedProcess.startObservation();
return observedProcess;
}
/**
* The observed process.
*/
public static interface ObservedProcess {
/**
* @return The process' name.
*/
String getName();
/**
* Submits a request to cancel an observed process.
*/
void cancel();
}
/**
* A handler that will be notified during the observation of the process.
*/
public static interface Handler {
/**
* Called if the process is started being observed.
*
* @param process The observed process.
* @param pm The progress monitor, that is used to monitor the progress of the running process.
*/
void onObservationStarted(ObservedProcess process, ProgressMonitor pm);
/**
* Called if a new text line that has been received from {@code stdout}.
*
* @param process The observed process.
* @param line The line.
* @param pm The progress monitor, that is used to monitor the progress of the running process.
*/
void onStdoutLineReceived(ObservedProcess process, String line, ProgressMonitor pm);
/**
* Called if a new text line that has been received from {@code stderr}.
*
* @param process The observed process.
* @param line The line.
* @param pm The progress monitor, that is used to monitor the progress of the running process.
*/
void onStderrLineReceived(ObservedProcess process, String line, ProgressMonitor pm);
/**
* Called if the process is no longer being observed.
*
* @param process The observed process.
* @param exitCode The exit code, may be {@code null} if unknown.
* @param pm The progress monitor, that is used to monitor the progress of the running process.
*/
void onObservationEnded(ObservedProcess process, Integer exitCode, ProgressMonitor pm);
}
/**
* Default implementation of {@link Handler}, which simply prints observations to the console.
*/
public static class DefaultHandler implements Handler {
@Override
public void onObservationStarted(ObservedProcess process, ProgressMonitor pm) {
System.out.println(process.getName() + " started");
}
@Override
public void onStdoutLineReceived(ObservedProcess process, String line, ProgressMonitor pm) {
System.out.println(process.getName() + ": " + line);
}
@Override
public void onStderrLineReceived(ObservedProcess process, String line, ProgressMonitor pm) {
System.err.println(process.getName() + ": " + line);
}
@Override
public void onObservationEnded(ObservedProcess process, Integer exitCode, ProgressMonitor pm) {
System.out.println(process.getName() + " ended, exit code " + exitCode);
}
}
private class ObservedProcessImpl implements ObservedProcess {
private ThreadGroup threadGroup;
private Thread stdoutThread;
private Thread stderrThread;
private boolean cancellationRequested;
private boolean cancelled;
ObservedProcessImpl() {
this.threadGroup = new ThreadGroup(name);
this.stdoutThread = new LineReaderThread(threadGroup, STDOUT);
this.stderrThread = new LineReaderThread(threadGroup, STDERR);
}
@Override
public String getName() {
return name;
}
@Override
public void cancel() {
cancellationRequested = true;
}
private void startObservation() {
handler.onObservationStarted(observedProcess, progressMonitor);
stdoutThread.start();
stderrThread.start();
if (mode == Mode.BLOCKING) {
awaitTermination();
} else /*if (mode == Mode.NON_BLOCKING)*/ {
Thread mainThread = new Thread(threadGroup,
new Runnable() {
@Override
public void run() {
awaitTermination();
}
},
name + "-" + MAIN);
mainThread.start();
}
}
private void awaitTermination() {
while (true) {
if ((progressMonitor.isCanceled() || cancellationRequested) && !cancelled) {
cancelled = true;
// todo - parametrise what is best done now:
// 1. just leave, and let the process be unattended
// * 2. destroy the process (current impl.)
// 3. throw a checked ProcessObserverException
process.destroy();
handler.onObservationEnded(this, null, progressMonitor);
break;
}
if (!stdoutThread.isAlive() && !stderrThread.isAlive()) {
try {
final int exitCode = process.exitValue();
handler.onObservationEnded(this, exitCode, progressMonitor);
break;
} catch (IllegalThreadStateException e) {
// process has not yet terminated, so continue observing
}
}
try {
Thread.sleep(pollPeriod);
} catch (InterruptedException e) {
// todo - parametrise what is best done now:
// * 1. just leave, and let the process be unattended (current impl.)
// 2. destroy the process
// 3. throw a checked ProcessObserverException
handler.onObservationEnded(this, null, progressMonitor);
break;
}
}
}
}
private class LineReaderThread extends Thread {
private final int type;
public LineReaderThread(ThreadGroup threadGroup, int type) {
super(threadGroup, name + "-" + type);
this.type = type;
}
@Override
public void run() {
try {
read();
} catch (IOException e) {
// cannot be handled in a meaningful way, but thread will end anyway
}
}
private void read() throws IOException {
final BufferedReader reader = new BufferedReader(
new InputStreamReader(type == STDOUT ? process.getInputStream() : process.getErrorStream()));
try {
String line;
while ((line = reader.readLine()) != null) {
fireLineReceived(line);
}
} finally {
reader.close();
}
}
private void fireLineReceived(String line) {
if (type == STDOUT) {
handler.onStdoutLineReceived(observedProcess, line, progressMonitor);
} else {
handler.onStderrLineReceived(observedProcess, line, progressMonitor);
}
}
}
}