();
cmd.add(java.getAbsolutePath());
cmd.add("-classpath");
cmd.add(classpath);
cmd.add(mainClass);
if (!LangUtil.isEmpty(args)) {
cmd.addAll(Arrays.asList(args));
}
String[] command = (String[]) cmd.toArray(new String[0]);
if (null == controller) {
controller = new ProcessController();
}
controller.init(command, mainClass);
return controller;
}
// /**
// * Create a process to run asynchronously.
// * @param controller if not null, initialize this one
// * @param command the String[] command to run
// * @param controller the ProcessControl for streams and results
// */
// public static ProcessController makeProcess( // not needed?
// ProcessController controller,
// String[] command,
// String label) {
// if (null == controller) {
// controller = new ProcessController();
// }
// controller.init(command, label);
// return controller;
// }
/**
* Find java executable File path from java.home system property.
*
* @return File associated with the java command, or null if not found.
*/
public static File getJavaExecutable() {
String javaHome = null;
File result = null;
// java.home
// java.class.path
// java.ext.dirs
try {
javaHome = System.getProperty("java.home");
} catch (Throwable t) {
// ignore
}
if (null != javaHome) {
File binDir = new File(javaHome, "bin");
if (binDir.isDirectory() && binDir.canRead()) {
String[] execs = new String[] { "java", "java.exe" };
for (int i = 0; i < execs.length; i++) {
result = new File(binDir, execs[i]);
if (result.canRead()) {
break;
}
}
}
}
return result;
}
// /**
// * Sleep for a particular period (in milliseconds).
// *
// * @param time the long time in milliseconds to sleep
// * @return true if delay succeeded, false if interrupted 100 times
// */
// public static boolean sleep(long milliseconds) {
// if (milliseconds == 0) {
// return true;
// } else if (milliseconds < 0) {
// throw new IllegalArgumentException("negative: " + milliseconds);
// }
// return sleepUntil(milliseconds + System.currentTimeMillis());
// }
/**
* Sleep until a particular time.
*
* @param time the long time in milliseconds to sleep until
* @return true if delay succeeded, false if interrupted 100 times
*/
public static boolean sleepUntil(long time) {
if (time == 0) {
return true;
} else if (time < 0) {
throw new IllegalArgumentException("negative: " + time);
}
// final Thread thread = Thread.currentThread();
long curTime = System.currentTimeMillis();
for (int i = 0; (i < 100) && (curTime < time); i++) {
try {
Thread.sleep(time - curTime);
} catch (InterruptedException e) {
// ignore
}
curTime = System.currentTimeMillis();
}
return (curTime >= time);
}
/**
* Handle an external process asynchrously. start()
launches a main thread to wait for the process and pipes
* streams (in child threads) through to the corresponding streams (e.g., the process System.err to this System.err). This can
* complete normally, by exception, or on demand by a client. Clients can implement doCompleting(..)
to get notice
* when the process completes.
*
* The following sample code creates a process with a completion callback starts it, and some time later retries the process.
*
*
* LangUtil.ProcessController controller = new LangUtil.ProcessController() {
* protected void doCompleting(LangUtil.ProcessController.Thrown thrown, int result) {
* // signal result
* }
* };
* controller.init(new String[] { "java", "-version" }, "java version");
* controller.start();
* // some time later...
* // retry...
* if (!controller.completed()) {
* controller.stop();
* controller.reinit();
* controller.start();
* }
*
*
* warning: Currently this does not close the input or output streams, since doing so prevents their use later.
*/
public static class ProcessController {
/*
* XXX not verified thread-safe, but should be. Known problems: - user stops (completed = true) then exception thrown from
* destroying process (stop() expects !completed) ...
*/
private String[] command;
private String[] envp;
private String label;
private boolean init;
private boolean started;
private boolean completed;
/** if true, stopped by user when not completed */
private boolean userStopped;
private Process process;
private FileUtil.Pipe errStream;
private FileUtil.Pipe outStream;
private FileUtil.Pipe inStream;
private ByteArrayOutputStream errSnoop;
private ByteArrayOutputStream outSnoop;
private int result;
private Thrown thrown;
public ProcessController() {
}
/**
* Permit re-running using the same command if this is not started or if completed. Can also call this when done with
* results to release references associated with results (e.g., stack traces).
*/
public final void reinit() {
if (!init) {
throw new IllegalStateException("must init(..) before reinit()");
}
if (started && !completed) {
throw new IllegalStateException("not completed - do stop()");
}
// init everything but command and label
started = false;
completed = false;
result = Integer.MIN_VALUE;
thrown = null;
process = null;
errStream = null;
outStream = null;
inStream = null;
}
public final void init(String classpath, String mainClass, String[] args) {
init(LangUtil.getJavaExecutable(), classpath, mainClass, args);
}
public final void init(File java, String classpath, String mainClass, String[] args) {
LangUtil.throwIaxIfNull(java, "java");
LangUtil.throwIaxIfNull(mainClass, "mainClass");
LangUtil.throwIaxIfNull(args, "args");
ArrayList cmd = new ArrayList();
cmd.add(java.getAbsolutePath());
cmd.add("-classpath");
cmd.add(classpath);
cmd.add(mainClass);
if (!LangUtil.isEmpty(args)) {
cmd.addAll(Arrays.asList(args));
}
init((String[]) cmd.toArray(new String[0]), mainClass);
}
public final void init(String[] command, String label) {
this.command = (String[]) LangUtil.safeCopy(command, new String[0]);
if (1 > this.command.length) {
throw new IllegalArgumentException("empty command");
}
this.label = LangUtil.isEmpty(label) ? command[0] : label;
init = true;
reinit();
}
public final void setEnvp(String[] envp) {
this.envp = (String[]) LangUtil.safeCopy(envp, new String[0]);
if (1 > this.envp.length) {
throw new IllegalArgumentException("empty envp");
}
}
public final void setErrSnoop(ByteArrayOutputStream snoop) {
errSnoop = snoop;
if (null != errStream) {
errStream.setSnoop(errSnoop);
}
}
public final void setOutSnoop(ByteArrayOutputStream snoop) {
outSnoop = snoop;
if (null != outStream) {
outStream.setSnoop(outSnoop);
}
}
/**
* Start running the process and pipes asynchronously.
*
* @return Thread started or null if unable to start thread (results available via getThrown()
, etc.)
*/
public final Thread start() {
if (!init) {
throw new IllegalStateException("not initialized");
}
synchronized (this) {
if (started) {
throw new IllegalStateException("already started");
}
started = true;
}
try {
process = Runtime.getRuntime().exec(command);
} catch (IOException e) {
stop(e, Integer.MIN_VALUE);
return null;
}
errStream = new FileUtil.Pipe(process.getErrorStream(), System.err);
if (null != errSnoop) {
errStream.setSnoop(errSnoop);
}
outStream = new FileUtil.Pipe(process.getInputStream(), System.out);
if (null != outSnoop) {
outStream.setSnoop(outSnoop);
}
inStream = new FileUtil.Pipe(System.in, process.getOutputStream());
// start 4 threads, process & pipes for in, err, out
Runnable processRunner = new Runnable() {
public void run() {
Throwable thrown = null;
int result = Integer.MIN_VALUE;
try {
// pipe threads are children
new Thread(errStream).start();
new Thread(outStream).start();
new Thread(inStream).start();
process.waitFor();
result = process.exitValue();
} catch (Throwable e) {
thrown = e;
} finally {
stop(thrown, result);
}
}
};
Thread result = new Thread(processRunner, label);
result.start();
return result;
}
/**
* Destroy any process, stop any pipes. This waits for the pipes to clear (reading until no more input is available), but
* does not wait for the input stream for the pipe to close (i.e., not waiting for end-of-file on input stream).
*/
public final synchronized void stop() {
if (completed) {
return;
}
userStopped = true;
stop(null, Integer.MIN_VALUE);
}
public final String[] getCommand() {
String[] toCopy = command;
if (LangUtil.isEmpty(toCopy)) {
return new String[0];
}
String[] result = new String[toCopy.length];
System.arraycopy(toCopy, 0, result, 0, result.length);
return result;
}
public final boolean completed() {
return completed;
}
public final boolean started() {
return started;
}
public final boolean userStopped() {
return userStopped;
}
/**
* Get any Throwable thrown. Note that the process can complete normally (with a valid return value), at the same time the
* pipes throw exceptions, and that this may return some exceptions even if the process is not complete.
*
* @return null if not complete or Thrown containing exceptions thrown by the process and streams.
*/
public final Thrown getThrown() { // cache this
return makeThrown(null);
}
public final int getResult() {
return result;
}
/**
* Subclasses implement this to get synchronous notice of completion. All pipes and processes should be complete at this
* time. To get the exceptions thrown for the pipes, use getThrown()
. If there is an exception, the process
* completed abruptly (including side-effects of the user halting the process). If userStopped()
is true, then
* some client asked that the process be destroyed using stop()
. Otherwise, the result code should be the
* result value returned by the process.
*
* @param thrown same as getThrown().fromProcess
.
* @param result same as getResult()
* @see getThrown()
* @see getResult()
* @see stop()
*/
protected void doCompleting(Thrown thrown, int result) {
}
/**
* Handle termination (on-demand, abrupt, or normal) by destroying and/or halting process and pipes.
*
* @param thrown ignored if null
* @param result ignored if Integer.MIN_VALUE
*/
private final synchronized void stop(Throwable thrown, int result) {
if (completed) {
throw new IllegalStateException("already completed");
} else if (null != this.thrown) {
throw new IllegalStateException("already set thrown: " + thrown);
}
// assert null == this.thrown
this.thrown = makeThrown(thrown);
if (null != process) {
process.destroy();
}
if (null != inStream) {
inStream.halt(false, true); // this will block if waiting
inStream = null;
}
if (null != outStream) {
outStream.halt(true, true);
outStream = null;
}
if (null != errStream) {
errStream.halt(true, true);
errStream = null;
}
if (Integer.MIN_VALUE != result) {
this.result = result;
}
completed = true;
doCompleting(this.thrown, result);
}
/**
* Create snapshot of Throwable's thrown.
*
* @param thrown ignored if null or if this.thrown is not null
*/
private final synchronized Thrown makeThrown(Throwable processThrown) {
if (null != thrown) {
return thrown;
}
return new Thrown(processThrown, (null == outStream ? null : outStream.getThrown()), (null == errStream ? null
: errStream.getThrown()), (null == inStream ? null : inStream.getThrown()));
}
public static class Thrown {
public final Throwable fromProcess;
public final Throwable fromErrPipe;
public final Throwable fromOutPipe;
public final Throwable fromInPipe;
/** true only if some Throwable is not null */
public final boolean thrown;
private Thrown(Throwable fromProcess, Throwable fromOutPipe, Throwable fromErrPipe, Throwable fromInPipe) {
this.fromProcess = fromProcess;
this.fromErrPipe = fromErrPipe;
this.fromOutPipe = fromOutPipe;
this.fromInPipe = fromInPipe;
thrown = ((null != fromProcess) || (null != fromInPipe) || (null != fromOutPipe) || (null != fromErrPipe));
}
public String toString() {
StringBuffer sb = new StringBuffer();
append(sb, fromProcess, "process");
append(sb, fromOutPipe, " stdout");
append(sb, fromErrPipe, " stderr");
append(sb, fromInPipe, " stdin");
if (0 == sb.length()) {
return "Thrown (none)";
} else {
return sb.toString();
}
}
private void append(StringBuffer sb, Throwable thrown, String label) {
if (null != thrown) {
sb.append("from " + label + ": ");
sb.append(LangUtil.renderExceptionShort(thrown));
sb.append(LangUtil.EOL);
}
}
} // class Thrown
}
}