From 81493c4accba30a77ef90256387688283ab2a016 Mon Sep 17 00:00:00 2001 From: wisberg Date: Mon, 5 May 2003 07:32:21 +0000 Subject: [PATCH] added LangUtil.ProcessController for external processes --- util/src/org/aspectj/util/LangUtil.java | 466 +++++++++++++++++++++++- 1 file changed, 463 insertions(+), 3 deletions(-) diff --git a/util/src/org/aspectj/util/LangUtil.java b/util/src/org/aspectj/util/LangUtil.java index 6e40f6ec4..56ce9b510 100644 --- a/util/src/org/aspectj/util/LangUtil.java +++ b/util/src/org/aspectj/util/LangUtil.java @@ -836,8 +836,8 @@ public class LangUtil { /** check if input contains any packages to elide. */ public static class StringChecker { - static StringChecker TEST_PACKAGES = new StringChecker( new String[] - { "org.aspectj.testing.", + static StringChecker TEST_PACKAGES = new StringChecker(new String[] + { "org.aspectj.testing", "org.eclipse.jdt.internal.junit", "junit.framework.", "org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner" @@ -860,5 +860,465 @@ public class LangUtil { } return result; } - } + } + + /** + * Gen classpath. + * @param bootclasspath + * @param classpath + * @param classesDir + * @param outputJar + * @return String combining classpath elements + */ + public static String makeClasspath( // XXX dumb implementation + String bootclasspath, + String classpath, + String classesDir, + String outputJar) { + StringBuffer sb = new StringBuffer(); + addIfNotEmpty(bootclasspath, sb, File.pathSeparator); + addIfNotEmpty(classpath, sb, File.pathSeparator); + if (!addIfNotEmpty(classesDir, sb, File.pathSeparator)) { + addIfNotEmpty(outputJar, sb, File.pathSeparator); + } + return sb.toString(); + } + + /** + * @param input ignored if null + * @param sink the StringBuffer to add input to - return false if null + * @param delimiter the String to append to input when added - ignored if empty + * @return true if input + delimiter added to sink + */ + private static boolean addIfNotEmpty(String input, StringBuffer sink, String delimiter) { + if (LangUtil.isEmpty(input) || (null == sink)) { + return false; + } + sink.append(input); + if (!LangUtil.isEmpty(delimiter)) { + sink.append(delimiter); + } + return true; + } + + + /** + * Create or initialize a process controller to run + * a process in another VM asynchronously. + * @param controller the ProcessController to initialize, if not null + * @param classpath + * @param mainClass + * @param args + * @return initialized ProcessController + */ + public static ProcessController makeProcess( + ProcessController controller, + String classpath, + String mainClass, + String[] args) { + File java = LangUtil.getJavaExecutable(classpath); + 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)); + } + 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. + * @param classpath ignored for now + * @return File associated with the java command, or null if not found. + */ + public static File getJavaExecutable(String classpath) { // XXX weak + 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; + } + + /** + * 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 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 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), + 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) { + LangUtil.throwIaxIfNotAssignable(command, String.class, "command"); + if (1 > command.length) { + throw new IllegalArgumentException("empty command"); + } + this.command = new String[command.length]; + System.arraycopy(command, 0, this.command, 0, command.length); + this.label = LangUtil.isEmpty(label) ? command[0] : label; + this.init = true; + reinit(); + } + + /** + * 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); + outStream = new FileUtil.Pipe(process.getInputStream(), System.out); + 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(true,true); + 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 + } // class ProcessController } + + -- 2.39.5