package plume;
import java.io.*;
import java.util.*;
import org.apache.commons.io.IOUtils;
/**
* TimeLimitProcess is a subclass of Process such that the process is
* killed if it runs for more than the specified number of milliseconds.
* Wall clock seconds, not CPU seconds, are measured.
* The process should already be started when TimeLimitProcess is invoked.
* Typical use:
* <pre>
* ProcessBuilder pb = ...;
* TimeLimitProcess p = new TimeLimitProcess(pb.start(), TIMEOUT_SEC * 1000);</pre>
*
* <b>Note</b>: If a Java process is destroyed (e.g., because it times
* out), then its output is unreadable: Java code trying to read its
* output stream fails. Here are two ways to get around this problem:
*
* <ul>
* <li>
* The client of TimeLimitProcess can send the process output to a file (or
* ByteArrayOutputStream, etc.), which can be read after the process
* terminates. This is easy to do in Java 7, for example via
* ProcessBuilder.redirectOutput(tempFile). There does not appear to be an
* easy way to do it in Java 6.
* <li>
* This class provides a workaround, in which it busy-waits reading the
* standard and error outputs and stores them away. Use
* ...
**/
public class TimeLimitProcess extends Process {
private Process p;
private long timeLimit;
private boolean timed_out;
// can make public for testing
private /*@LazyNonNull*/ StringWriter cached_stdout;
private /*@LazyNonNull*/ StringWriter cached_stderr;
private Timer timer;
private static boolean debug = false;
/**
* Creates a TimeLimitProcess with the given time limit, in wall clock
* milliseconds.
* Requires: p != null
* @param timeLimit in milliseconds
**/
public TimeLimitProcess (Process p, long timeLimit) {
this(p, timeLimit, false);
}
/**
* Creates a TimeLimitProcess with the given time limit, in wall clock
* milliseconds.
* Requires: p != null
* @param timeLimit in milliseconds
* @param cacheStdout
* If true, causes the TimeLimitProcess to consume the standard output of the
* underlying process, and to cache it. After the process terminates (on
* its own or by being timed out), the output is available via the
* cached_stdout method. This is necessary because when a Java process
* is terminated, its standard output is no longer available.
*/
public TimeLimitProcess (Process p, long timeLimit, boolean cacheStdout) {
this.p = p;
timer = new Timer(true);
this.timeLimit = timeLimit;
if (debug) {
System.out.printf("new timelimit process, timeLimit=%s, cacheStdout=%s%n",
timeLimit, cacheStdout);
}
timer.schedule(new TPTimerTask(this, timeLimit), timeLimit);
if (cacheStdout) {
cached_stdout = new StringWriter();
cached_stderr = new StringWriter();
new StdoutStreamReaderThread().start();
new StderrStreamReaderThread().start();
}
}
/**
* Returns true if the process has timed out (has run for more than the
* timeLimit msecs specified in the constructor).
*/
public boolean timed_out() {
return (timed_out);
}
/**
* Returns the timeout time in msecs.
*/
public long timeout_msecs() {
return (timeLimit);
}
// /**
// * Returns the standard output of the process, if the cacheStdout
// * parameter was "true" when the constructor was invoked.
// * Only for debugging.
// */
// public String cached_stdout() {
// if (cached_stdout == null) {
// throw new Error("called cached_stdout() without previously calling cache_stdout()");
// }
// return cached_stdout.toString();
// }
/**
* Kills the subprocess.
* @see Process#destroy()
**/
public void destroy() {
p.destroy();
}
/**
* Returns the exit value for the subprocess.
* @see Process#getErrorStream()
*/
public int exitValue() {
// I'm not sure whether this is necessary; the Process.destroy()
// documentation doesn't specify the effect on the exit value.
if ((p.exitValue() == 0) && timed_out) {
return 255;
} else {
return p.exitValue();
}
}
/**
* Gets the error stream of the subprocess.
* @see Process#getErrorStream()
*/
public InputStream getErrorStream() {
if (cached_stderr == null) {
return p.getErrorStream();
} else {
// Convert a String to an InputStream
String text = cached_stderr.toString();
try {
InputStream is = new ByteArrayInputStream(text.getBytes("UTF-8"));
return is;
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
}
/**
* Gets the input stream of the subprocess.
* @see Process#getInputStream()
*/
public InputStream getInputStream() {
if (cached_stdout == null) {
return p.getInputStream();
} else {
return stringToInputStream(cached_stdout.toString());
}
}
// Convert a String to an InputStream
private InputStream stringToInputStream(String text) {
try {
InputStream is = new ByteArrayInputStream(text.getBytes("UTF-8"));
return is;
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
/**
* Gets the output stream of the subprocess.
* @see Process#getOutputStream()
*/
public OutputStream getOutputStream() {
return p.getOutputStream();
}
/**
* Causes the current thread to wait, if necessary, until the process represented by this Process object has terminated.
* @see Process#waitFor()
*/
public int waitFor() throws InterruptedException {
return p.waitFor();
}
/**
* @return true if the process if finished, false otherwise
**/
public boolean finished () {
try {
// Process.exitValue() throws an exception if the process is not
// finished.
p.exitValue();
return true;
} catch (IllegalThreadStateException ie) {
return false;
}
}
/**
* This TimerTask destroys the process that is passed to it.
**/
private static class TPTimerTask extends TimerTask {
TimeLimitProcess tp;
long timeLimit;
public TPTimerTask(TimeLimitProcess tp, long timeLimit) {
this.tp = tp;
this.timeLimit = timeLimit;
}
public void run() {
// If exitValue is queried while the process is still running,
// the IllegalThreadStateException will be thrown. If that
// happens, we kill the process and note that so callers can
// tell that a timeout occurred.
try {
int exit = tp.p.exitValue();
if (debug) {
System.out.println();
System.out.println("Process exited with status " + exit);
System.out.println();
}
} catch (IllegalThreadStateException ie) {
tp.p.destroy();
tp.timed_out = true;
if (debug) {
System.out.println("Terminated process after timelimit of "
+ timeLimit + " msecs expired");
System.out.println();
}
}
this.cancel();
}
}
// I'm not sure how to generalize the below two classes into one -- my
// attempt failed.
private class StdoutStreamReaderThread extends Thread {
/*@NonNullOnEntry("cached_stdout")*/
public void run() {
// This thread will block as the process produces output. That's OK,
// because the blocking is happening in a separate thread.
try {
IOUtils.copy(p.getInputStream(), cached_stdout);
} catch (IOException e) {
// assume the best
}
}
}
private class StderrStreamReaderThread extends Thread {
/*@NonNullOnEntry("cached_stderr")*/
public void run() {
// This thread will block as the process produces output. That's OK,
// because the blocking is happening in a separate thread.
try {
IOUtils.copy(p.getErrorStream(), cached_stderr);
} catch (IOException e) {
// assume the best
}
}
}
}