/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package edu.mit.csail.sdg.alloy4; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.PrintStream; import java.io.Serializable; import java.lang.Thread.UncaughtExceptionHandler; /** This class allows you to execute tasks in a subprocess, and receive its outputs via callback. * * <p> By executing the task in a subprocess, we can always terminate a runaway task explicitly by calling stop(), * and we can control how much memory and stack space to give to the subprocess. * * <p> Only one task may execute concurrently at any given time; if you try to issue a new task * when the previous task hasn't finished, then you will get an IOException. * * <p> As long as the subprocess hasn't terminated either due to crashing or due to user calling stop(), * then the same subprocess is reused to execute each subsequent task; however, if the subprocess crashed, * the crash will be reported to the parent process via callback, and if we try to execute another task, * then a new subprocess will be spawned automatically. */ public final class WorkerEngine { /** This defines an interface for performing tasks in a subprocess. */ public interface WorkerTask extends Serializable { /** The task should send zero or more non-null Objects to out.callback(msg) to report progress to the parent process. */ public void run(WorkerCallback out) throws Exception; } /** This defines an interface for receiving results from a subprocess. */ public interface WorkerCallback { /** The task would send zero or more non-null Objects to this handler * (the objects will be serialized by the sub JVM and deserialized in the parent JVM). */ public void callback(Object msg); /** If the task completed successfully, this method will be called. */ public void done(); /** If the task terminated with an error, this method will be called. */ public void fail(); } /** This wraps the given InputStream such that the resulting object's "close()" method does nothing; * if stream==null, we get an InputStream that always returns EOF. */ private static InputStream wrap(final InputStream stream) { return new InputStream() { public int read(byte b[], int off, int len) throws IOException { if (len==0) return 0; else if (stream==null) return -1; else return stream.read(b, off, len); } public int read() throws IOException { if (stream==null) return -1; else return stream.read(); } public long skip(long n) throws IOException { if (stream==null) return 0; else return stream.skip(n); } }; } /** This wraps the given OutputStream such that the resulting object's "close()" method simply calls "flush()"; * if stream==null, we get an OutputStream that ignores all writes. */ private static OutputStream wrap(final OutputStream stream) { return new OutputStream() { public void write(int b) throws IOException { if (stream!=null) stream.write(b); } public void write(byte b[], int off, int len) throws IOException { if (stream!=null) stream.write(b, off, len); } public void flush() throws IOException { if (stream!=null) stream.flush(); } public void close() throws IOException { if (stream!=null) stream.flush(); } // The close() method above INTENTIONALLY does not actually close the file }; } /** If nonnull, it is the latest sub JVM. */ private static Process latest_sub = null; /** If nonnull, it is the latest worker thread talking to the sub JVM. * (If latest_sub==null, then we guarantee latest_manager is also null) */ private static Thread latest_manager = null; /** Constructor is private since this class does not need to be instantiated. */ private WorkerEngine() { } /** This terminates the subprocess, and prevent any further results from reaching the parent's callback handler. */ public static void stop() { synchronized(WorkerEngine.class) { try { if (latest_sub!=null) latest_sub.destroy(); } finally { latest_manager=null; latest_sub=null; } } } /** This returns true iff the subprocess is still busy processing the last task. */ public static boolean isBusy() { synchronized(WorkerEngine.class) { return latest_manager!=null && latest_manager.isAlive(); } } /** This executes a task using the current thread. * @param task - the task that we want to execute * @param callback - the handler that will receive outputs from the task * @throws IOException - if a previous task is still busy executing */ public static void runLocally(final WorkerTask task, final WorkerCallback callback) throws Exception { synchronized(WorkerEngine.class) { if (latest_manager!=null && latest_manager.isAlive()) throw new IOException("Subprocess still performing the last task."); try { task.run(callback); callback.done(); } catch(Throwable ex) { callback.callback(ex); callback.fail(); } } } /** This issues a new task to the subprocess; * if subprocess hasn't been constructed yet or has terminated abnormally, this method will launch a new subprocess. * @param task - the task that we want the subprocess to execute * @param newmem - the amount of memory (in megabytes) we want the subprocess to have * (if the subproces has not terminated, then this parameter is ignored) * @param newstack - the amount of stack (in kilobytes) we want the subprocess to have * (if the subproces has not terminated, then this parameter is ignored) * @param jniPath - if nonnull and nonempty, then it specifies the subprocess's default JNI library location * @param classPath - if nonnull and nonempty, then it specifies the subprocess's default CLASSPATH, * else we'll use System.getProperty("java.class.path") * @param callback - the handler that will receive outputs from the task * @throws IOException - if a previous task is still busy executing * @throws IOException - if an error occurred in launching a sub JVM or talking to it */ public static void run (final WorkerTask task, int newmem, int newstack, String jniPath, String classPath, final WorkerCallback callback) throws IOException { if (classPath==null || classPath.length()==0) classPath = System.getProperty("java.class.path"); synchronized(WorkerEngine.class) { final Process sub; if (latest_manager!=null && latest_manager.isAlive()) throw new IOException("Subprocess still performing the last task."); try { if (latest_sub!=null) latest_sub.exitValue(); latest_manager=null; latest_sub=null; } catch(IllegalThreadStateException ex) { } if (latest_sub==null) { String java = "java", javahome = System.getProperty("java.home"); if (javahome!=null && javahome.length()>0) { // First try "[JAVAHOME]/bin/java" File f = new File(javahome + File.separatorChar + "bin" + File.separatorChar + "java"); // Then try "[JAVAHOME]/java" if (!f.isFile()) f = new File(javahome + File.separatorChar + "java"); // All else, try "java" (and let the Operating System search the program path...) if (f.isFile()) java = f.getAbsolutePath(); } String debug = "yes".equals(System.getProperty("debug")) ? "yes" : "no"; if (jniPath!=null && jniPath.length()>0) sub = Runtime.getRuntime().exec(new String[] { java, "-Xmx" + newmem + "m", "-Xss" + newstack + "k", "-Djava.library.path=" + jniPath, "-Ddebug=" + debug, "-cp", classPath, WorkerEngine.class.getName(), Version.buildDate(), ""+Version.buildNumber() }); else sub = Runtime.getRuntime().exec(new String[] { java, "-Xmx" + newmem + "m", "-Xss" + newstack + "k", "-Ddebug=" + debug, "-cp", classPath, WorkerEngine.class.getName(), Version.buildDate(), ""+Version.buildNumber() }); latest_sub = sub; } else { sub = latest_sub; } latest_manager = new Thread(new Runnable() { public void run() { ObjectInputStream sub2main = null; ObjectOutputStream main2sub = null; try { main2sub = new ObjectOutputStream(wrap(sub.getOutputStream())); main2sub.writeObject(task); main2sub.close(); sub2main = new ObjectInputStream(wrap(sub.getInputStream())); } catch(Throwable ex) { sub.destroy(); Util.close(main2sub); Util.close(sub2main); synchronized(WorkerEngine.class) { if (latest_sub != sub) return; callback.fail(); return; } } while(true) { synchronized(WorkerEngine.class) { if (latest_sub != sub) return; } Object x; try { x = sub2main.readObject(); } catch(Throwable ex) { sub.destroy(); Util.close(sub2main); synchronized(WorkerEngine.class) { if (latest_sub != sub) return; callback.fail(); return; } } synchronized(WorkerEngine.class) { if (latest_sub != sub) return; if (x==null) {callback.done(); return;} else callback.callback(x); } } } }); latest_manager.start(); } } /** This is the entry point for the sub JVM. * * <p> Behavior is very simple: it reads a WorkerTask object from System.in, then execute it, then read another... * If any error occurred, or if it's disconnected from the parent process's pipe, it then terminates itself * (since we assume the parent process will notice it and react accordingly) */ public static void main(String[] args) { // To prevent people from accidentally invoking this class, or invoking it from an incompatible version, // we add a simple sanity check on the command line arguments if (args.length!=2) halt("#args should be 2 but instead is "+args.length, 1); if (!args[0].equals(Version.buildDate())) halt("BuildDate mismatch: "+args[0]+" != "+Version.buildDate(), 1); if (!args[1].equals("" + Version.buildNumber())) halt("BuildNumber mismatch: "+args[1]+" != "+Version.buildNumber(), 1); // To prevent a zombie process, we set a default handler to terminate itself if something does slip through our detection Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { halt("UncaughtException: "+e, 1); } }); // Redirect System.in, System.out, System.err to no-op (so that if a task tries to read/write to System.in/out/err, // those reads and writes won't mess up the ObjectInputStream/ObjectOutputStream) System.setIn(wrap((InputStream)null)); System.setOut(new PrintStream(wrap((OutputStream)null))); System.setErr(new PrintStream(wrap((OutputStream)null))); final FileInputStream in = new FileInputStream(FileDescriptor.in); final FileOutputStream out = new FileOutputStream(FileDescriptor.out); // Preload these 3 libraries; on MS Windows with JDK 1.6 this seems to prevent freezes try { System.loadLibrary("minisat"); } catch(Throwable ex) { } try { System.loadLibrary("minisatprover"); } catch(Throwable ex) { } try { System.loadLibrary("zchaff"); } catch(Throwable ex) { } // Now we repeat the following read-then-execute loop Thread t = null; while(true) { final WorkerTask task; try { System.gc(); // while we're waiting for the next task, we might as well encourage garbage collection ObjectInputStream oin = new ObjectInputStream(wrap(in)); task = (WorkerTask) oin.readObject(); oin.close(); } catch(Throwable ex) { halt("Can't read task: "+ex, 1); return; } // Our main thread has a loop that keeps "attempting" to read bytes from System.in, // and delegate the actual task to a separate "worker thread". // This way, if the parent process terminates, then this subprocess should see it almost immediately // (since the inter-process pipe will be broken) and will terminate (regardless of the status of the worker thread) if (t!=null && t.isAlive()) { // We only get here if the previous subtask has informed the parent that the job is done, and that the parent // then issued another job. So we wait up to 5 seconds for the worker thread to confirm its termination. // If 5 seconds is up, then we assume something terrible has happened. try {t.join(5000); if (t.isAlive()) halt("Timeout", 1);} catch (Throwable ex) {halt("Timeout: "+ex, 1);} } t = new Thread(new Runnable() { public void run() { ObjectOutputStream x = null; Throwable e = null; try { x = new ObjectOutputStream(wrap(out)); final ObjectOutputStream xx = x; WorkerCallback y = new WorkerCallback() { public void callback(Object x) { try {xx.writeObject(x);} catch(IOException ex) {halt("Callback: "+ex, 1);} } public void done() { } public void fail() { } }; task.run(y); x.writeObject(null); x.flush(); } catch(Throwable ex) { e=ex; } for(Throwable t=e; t!=null; t=t.getCause()) if (t instanceof OutOfMemoryError || t instanceof StackOverflowError) { try { System.gc(); x.writeObject(t); x.flush(); } catch(Throwable ex2) { } finally { halt("Error: "+e, 2); } } if (e instanceof Err) { try { System.gc(); x.writeObject(e); x.writeObject(null); x.flush(); } catch(Throwable t) { halt("Error: "+e, 1); } } if (e!=null) { try { System.gc(); x.writeObject(e); x.flush(); } catch(Throwable t) { } finally { halt("Error: "+e, 1); } } Util.close(x); // avoid memory leaks } }); t.start(); } } /** This method terminates the caller's process. */ private static void halt(String reason, int exitCode) { Runtime.getRuntime().halt(exitCode); } }