/*
This file is part of the BlueJ program.
Copyright (C) 1999-2009,2010,2011 Michael Kolling and John Rosenberg
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
This file is subject to the Classpath exception as provided in the
LICENSE.txt file that accompanied this code.
*/
package bluej.runtime;
import java.awt.AWTEvent;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.AWTEventListener;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.junit.runner.JUnitCore;
import org.junit.runner.Request;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import bluej.utility.Utility;
/**
* Class that controls the runtime of code executed within BlueJ.
* Sets up the initial thread state, etc.
*
* This class both holds runtime attributes and executes commands.
* Execution is done through JDI reflection from the JdiDebugger class.
*
* @author Michael Kolling
* @author Andrew Patterson
*/
public class ExecServer
{
// these fields will be fetched by VMReference
// the initial thread that starts main()
public static final String MAIN_THREAD_NAME = "mainThread";
public static Thread mainThread = null;
// a worker thread that we create
public static final String WORKER_THREAD_NAME = "workerThread";
public static Thread workerThread = null;
// Parameters for main thread actions
public static String classToRun;
public static String methodToRun;
public static String [] parameterTypes;
public static Object [] arguments;
public static int execAction = -1; // EXEC_SHELL, TEST_SETUP or TEST_RUN
public static Object methodReturn;
public static Class<?> executedClass;
public static Throwable exception;
// These constant values must match the variable names declared above
public static final String CLASS_TO_RUN_NAME = "classToRun";
public static final String METHOD_TO_RUN_NAME = "methodToRun";
public static final String PARAMETER_TYPES_NAME = "parameterTypes";
public static final String ARGUMENTS_NAME = "arguments";
public static final String EXEC_ACTION_NAME = "execAction";
public static final String METHOD_RETURN_NAME = "methodReturn";
public static final String EXCEPTION_NAME = "exception";
public static final String EXECUTED_CLASS_NAME = "executedClass";
// Possible actions for the main thread
public static final int EXEC_SHELL = 0; // Execute a shell class
public static final int TEST_SETUP = 1;
public static final int TEST_RUN = 2;
public static final int DISPOSE_WINDOWS = 3;
public static final int EXIT_VM = 4;
public static final int LOAD_INIT_CLASS = 5; // load and initialize a class
public static final int INSTANTIATE_CLASS = 6; // use default constructor
public static final int INSTANTIATE_CLASS_ARGS = 7; // use constructor
// with specified parameter types and arguments
// Parameter for worker thread actions
public static int workerAction = EXIT_VM;
public static String objectName;
public static Object object;
public static String classPath;
public static String className;
public static String scopeId;
public static ClassLoader classLoader = null; // null to use current loader.
public static Object workerReturn;
// These constant values must match the variable names declared above
public static final String WORKER_ACTION_NAME = "workerAction";
public static final String OBJECTNAME_NAME = "objectName";
public static final String OBJECT_NAME = "object";
public static final String CLASSPATH_NAME = "classPath";
public static final String CLASSNAME_NAME = "className";
public static final String WORKER_RETURN_NAME = "workerReturn";
public static final String SCOPE_ID_NAME = "scopeId";
public static final String CLASSLOADER_NAME = "classLoader";
// possible actions for worker thread
public static final int REMOVE_OBJECT = 0;
public static final int ADD_OBJECT = 1;
public static final int LOAD_CLASS = 2;
public static final int NEW_LOADER = 3;
// EXIT_VM ( = 4) is also used in the worker thread
public static final int LOAD_ALL = 5; // load class and inner classes
// the current class loader
private static ClassLoader currentLoader;
// The loader that loads the greenfoot application classes. This is the
// loader that gets used the first time anything is loaded in the debugvm.
// This loader will be the parent loader for all loaders created later on.
// The reason we need to do this, is to keep using the same class for
// GreenfootObject and GreenfootWorld in order to cast newly created objects
// into these types.
//private static ClassLoader greenfootLoader;
// a hashmap of names to objects
// private static Map objects = new HashMap();
private static Map<String,BJMap<String,Object>> objectMaps = new HashMap<String,BJMap<String,Object>>();
/**
* We need to keep track of open windows so that we can dispose of them
* when simulating a System.exit() call
*/
private static List<Window> openWindows = Collections.synchronizedList(new LinkedList<Window>());
private static boolean disposingAllWindows = false; // true while we are disposing
/**
* Main method.
*/
public static void main(String[] args)
throws Throwable
{
// Set up an input stream filter to detect "End of file" signals
// (CTRL-Z or CTRL-D typed in terminal)
System.setIn(new BJInputStream(System.in));
// Set up encoding for the terminal, the only arg that should be passed in
// is the encoding eg. "UTF-8, otherwise do nothing
if(args.length > 0 && !args[0].equals("")) {
try {
System.setOut(new PrintStream(System.out, true, args[0]));
}
catch (UnsupportedEncodingException uee) {
// Do nothing; don't use the requested encoding
}
}
// Set up the worker thread. The worker thread can be used to perform certain actions
// when the main thread is busy. Actions on the worker thread are guarenteed to execute
// in a timely manner - for this reason they must not execute user code.
workerThread = new Thread("BlueJ worker thread")
{
public void run()
{
while(true) {
vmSuspend();
switch(workerAction) {
case ADD_OBJECT:
addObject(scopeId, objectName, object);
object = null;
break;
case REMOVE_OBJECT:
removeObject(scopeId, objectName);
break;
case LOAD_CLASS:
try {
if (classLoader == null)
classLoader = currentLoader;
workerReturn = Class.forName(className, false, currentLoader);
// Cause the class to be prepared (ie. its fields and methods
// enumerated). Otherwise we can get ClassNotPreparedException
// when we try and get the fields on the other VM.
((Class<?>) workerReturn).getFields();
classLoader = null; // reset for next call
}
catch(Throwable cnfe) {
workerReturn = null;
}
break;
case NEW_LOADER:
workerReturn = newLoader(classPath);
break;
case EXIT_VM:
System.exit(0);
case LOAD_ALL:
workerReturn = loadAllClasses(className);
}
// After any action, set the next action to exit. If connection to
// primary VM is lost, the secondary VM (i.e. this VM) will then exit.
workerAction = EXIT_VM;
}
}
};
// register a listener to record all window opens and closes
Toolkit toolkit = Toolkit.getDefaultToolkit();
AWTEventListener listener = new AWTEventListener()
{
public void eventDispatched(AWTEvent event)
{
Object source = event.getSource();
if(event.getID() == WindowEvent.WINDOW_OPENED) {
if (source instanceof Window) {
addWindow((Window) source);
Utility.bringToFront((Window) source);
}
} else if(event.getID() == WindowEvent.WINDOW_CLOSED) {
if (source instanceof Window) {
removeWindow((Window) source);
}
}
}
};
toolkit.addAWTEventListener(listener, AWTEvent.WINDOW_EVENT_MASK);
// signal with a breakpoint that we have performed our VM
// initialization, at the same time, create the initial server thread.
newThread();
// Set the worker thread in motion also. Give it maximum priority so that it can
// be guarenteed to execute in a timely manner, and won't get starved by user code
// executing in other threads.
workerThread.setPriority(Thread.MAX_PRIORITY);
workerThread.start();
}
/**
* This method is used to suspend the execution of the
* machine to indicate that everything is up and running.
*/
public static void vmStarted()
{
// <SUSPENDING BREAKPOINT!>
}
/**
* This method is used to suspend the execution of the worker threads.
* This is done via a breakpoint: a breakpoint is set in this method
* so calling this method suspends execution.
*/
public static void vmSuspend()
{
// <SUSPENDING BREAKPOINT!>
}
/**
* Add the object to our list of open windows
*
* @param o a window object which has just been opened
*/
private static void addWindow(Window o)
{
openWindows.add(o);
}
/**
* Remove the object from our list of open windows
*
* @param o a window object which has just been closed
*/
private static void removeWindow(Window o)
{
if(!disposingAllWindows) // don't bother if we are clearing up just now
openWindows.remove(o);
}
/**
* Find a scoping Map for the given scopeId
*/
static BJMap<String,Object> getScope(String scopeId)
{
synchronized (objectMaps) {
BJMap<String,Object> m = objectMaps.get(scopeId);
if (m == null) {
m = new BJMap<String,Object>();
objectMaps.put(scopeId, m);
}
return m;
}
}
/**
* Create a new class loader for a given classpath.
* @param urlListAsString a URL list written as a single string (the \n is used to divide entries)
* @return a URLClassLoader that can be used to load user classes.
*/
private static ClassLoader newLoader(String urlListAsString )
{
String [] splits = urlListAsString.split("\n");
URL []urls = new URL[splits.length];
for (int index = 0; index < splits.length; index++)
try {
urls[index] = new URL(splits[index]);
}
catch (MalformedURLException mfue) {
// Should never happen but if it does we want to know about it
System.err.println("ExecServer.newLoader() Malformed URL=" + splits[index]);
}
currentLoader = new URLClassLoader(urls);
synchronized (objectMaps) {
objectMaps.clear();
}
return currentLoader;
}
/**
* Load (and prepare) a class in the remote runtime. Return null if the class could not
* be loaded.
*/
public static Class<?> loadAndInitClass(String className)
{
Throwable exception = null;
Class<?> cl;
try {
cl = Class.forName(className, true, currentLoader);
}
catch (ClassNotFoundException cnfe) {
// class definitely doesn't exist
cl = null;
}
catch (ExceptionInInitializerError eiie) {
// The class was loaded it, but an exception occurred during initialization.
// As this is an error in user code, we want to report it.
exception = eiie.getCause();
// Now get the class again, uninitialized this time
try {
cl = Class.forName(className, false, currentLoader);
}
catch (ClassNotFoundException cnfe) {
// this shouldn't happen anyway.
cl = null;
}
}
catch (Throwable err) {
// There are numerous other linkage problems. Also there is the possibility that
// a static initialization block will throw an instance of java.lang.Error, which
// will not be wrapped in an ExceptionInInitializerError (unfortunately). In either
// case we probably should let the user know what happened.
exception = err;
// The class may exist, but not be initializable for some reason.
try {
cl = Class.forName(className, false, currentLoader);
}
catch (Throwable t) {
cl = null;
}
}
// If we have an exception to report, filter and report it.
if (exception != null) {
StackTraceElement [] stackTrace = exception.getStackTrace();
// filter bluej.runtime.ExecServer from the stack trace
int i;
for (i = stackTrace.length - 1; i > 0; i--) {
String stClassName = stackTrace[i].getClassName();
if (! stClassName.startsWith("bluej.runtime.ExecServer")
&& ! stClassName.startsWith("java.lang.Class"))
break;
}
StackTraceElement [] newStackTrace = new StackTraceElement[i+1];
System.arraycopy(stackTrace, 0, newStackTrace, 0, i+1);
exception.setStackTrace(newStackTrace);
recordException(exception);
}
return cl;
}
/**
* Load a class, and all its inner classes.
*/
private static Class<?>[] loadAllClasses(String className)
{
List<Class<?>> l = new ArrayList<Class<?>>();
try {
Class<?> c = currentLoader.loadClass(className);
c.getFields(); // prepare class
l.add(c);
getDeclaredInnerClasses(c, l);
// Now we want the anonymous inner classes:
int i = 1;
while(true) {
c = currentLoader.loadClass(className + '$' + i);
c.getFields();
l.add(c);
i++;
}
}
catch (Throwable t) {}
return l.toArray(new Class[l.size()]);
}
/**
* Add the declared inner classes of the given class to the given
* list, recursively.
*/
private static void getDeclaredInnerClasses(Class<?> c, List<Class<?>> list)
{
try {
Class<?> [] rlist = c.getDeclaredClasses();
for (int i = 0; i < rlist.length; i++) {
c = rlist[i];
c.getFields(); // force preparation
list.add(rlist[i]);
getDeclaredInnerClasses(rlist[i], list);
}
}
catch (Throwable t) {}
}
/**
* Add an object into a package scope (for possible use as parameter
* later). Used after object creation to add the newly created object
* to the scope.
*
* Must be static because it is used by Shell without a execServer reference
*/
static void addObject(String scopeId, String instanceName, Object value)
{
// Debug.message("[VM] addObject: " + instanceName + " " + value);
BJMap<String,Object> scope = getScope(scopeId);
synchronized (scope) {
scope.put(instanceName, value);
scope.notify(); // in case Greenfoot is waiting for this object
}
}
/**
* Execute a JUnit test case setUp method.
*
* @return an array consisting of String, Object pairs. For n fixture objects
* there will be n*2 entries in the array. Putting it in an array saves
* having to make lots of reflective List and HashMap calls on the
* calling virtual machine. Once the calling VM gets this array it can
* put it into a more suitable data structure itself.
*/
private static Object[] runTestSetUp(String className)
{
// Debug.message("[VM] runTestSetUp" + className);
Class<?> cl = loadAndInitClass(className);
try {
// construct an instance of the test case (firstly trying the
// String argument constructor - then the no-arg constructor)
Object testCase = null;
Class<?> [] partypes = new Class[1];
partypes[0] = String.class;
try {
Constructor<?> ct = cl.getConstructor(partypes);
Object arglist[] = new Object[1];
arglist[0] = "TestCase " + className;
testCase = ct.newInstance(arglist);
}
catch(NoSuchMethodException nsme) {
testCase = null;
}
if (testCase == null) {
testCase = cl.newInstance();
}
// cannot execute setUp directly because it is protected
// we can however use reflection to call it because this VM
// has access protection disabled
Method setUpMethod = findMethod(cl, "setUp", null);
if (setUpMethod != null) {
setUpMethod.setAccessible(true);
setUpMethod.invoke(testCase, (Object []) null);
}
// pick up all declared fields
// this will not get inherited fields!! (would need to deal
// with them some other way)
Field fields[] = cl.getDeclaredFields();
// we make it one bigger than double the number of fields to store the
// test case object which is used later for extracting (possibly generic) fields
// whose exact generic types may not be available via class level
// reflection
Object obs[] = new Object[fields.length*2 + 1];
for(int i=0; i<fields.length; i++) {
// make sure we can access the field regardless of protection
fields[i].setAccessible(true);
// fill in the return array in the format
// name, object, name, object
obs[i*2] = fields[i].getName();
obs[i*2+1] = fields[i].get(testCase);
}
//add the testcase as the last object in the array
obs[obs.length-1] = testCase;
return obs;
}
catch (Throwable e) {
e.printStackTrace();
}
return new Object[0];
}
/**
* Find a method in the class, regardless of visibility. This is
* essentially the same as Class.getMethod(), except that it also returns
* non-public methods.
*
* @param cl The class to search
* @param name The name of the method
* @param paramtypes The argument types
* @return The method, or null if not found.
*/
static private Method findMethod(Class<?> cl, String name, Class<?>[] paramtypes)
{
while (cl != null) {
try {
return cl.getDeclaredMethod(name, paramtypes);
}
catch (NoSuchMethodException nsme) {}
cl = cl.getSuperclass();
}
return null;
}
/**
* Execute a JUnit test method and return the result.<p>
*
* The array returned in case of failure or error contains:<br>
* [0] = the runtime in milliseconds expressed as a decimal integer
* [1] = the exception message (or "no exception message")<br>
* [2] = the stack trace as a string (or "no stack trace")<br>
* [3] = the name of the class in which the exception/failure occurred<br>
* [4] = the source filename for where the exception/failure occurred<br>
* [5] = the name of the method in which the exception/failure occurred<br>
* [6] = the line number where the exception/failure occurred (a string)<br>
* [7] = "failure" or "error" (string)<br>
*
* The array returned in case of success contains:<br>
* [0] = the runtime in milliseconds expressed as a decimal integer
*
* @return an array of length 8 on test failure/error, or of length 1 if the test passed
*/
private static Object[] runTestMethod(String className, String methodName)
{
Class<?> cl = loadAndInitClass(className);
Result res = (new JUnitCore()).run(Request.method(cl, methodName));
if (res.wasSuccessful()) {
Object result[] = new Object[1];
result[0] = String.valueOf(res.getRunTime());
return result;
} else {
Object result[] = new Object[8];
List<Failure> failures = res.getFailures();
for (Iterator<Failure> iterator = failures.iterator(); iterator.hasNext();) {
Failure failure = iterator.next();
if (failure.getException().getClass() == java.lang.AssertionError.class
|| failure.getException().getClass() == junit.framework.AssertionFailedError.class) {
result[7] = "failure";
}
else {
result[7] = "error";
}
result[0] = String.valueOf(res.getRunTime());
result[1] = failure.getMessage() != null ? failure.getMessage() : "no exception message";
result[2] = failure.getTrace() != null ? failure.getTrace() : "no trace";
// search the stack trace backward until finding a class not
// part of the org.junit framework
StackTraceElement [] ste = failure.getException().getStackTrace();
int i = 0;
while(i < ste.length && ste[i].getClassName().startsWith("org.junit.")) {
i++;
}
result[3] = ste[i].getClassName();
result[4] = ste[i].getFileName();
result[5] = ste[i].getMethodName();
result[6] = String.valueOf(ste[i].getLineNumber());
}
return result;
}
}
/**
* Remove an object from the scope.
*/
private static void removeObject(String scopeId, String instanceName)
{
//Debug.message("[VM] removeObject: " + instanceName);
BJMap<String,Object> scope = getScope(scopeId);
synchronized (scope) {
scope.remove(instanceName);
}
}
/**
* Dispose of all the top level windows we think are open.
*
* Must be static because it is used by RemoteSecurityManager without a execServer reference
*/
private static void disposeWindows()
{
synchronized(openWindows) {
disposingAllWindows = true;
Iterator<Window> it = openWindows.iterator();
while(it.hasNext()) {
it.next().dispose();
}
openWindows.clear();
disposingAllWindows = false;
}
}
/**
* Clear the system input buffer. This is used between method calls to
* make sure that System.in.read() doesn't read input which was buffered
* during the last method call but never read.
*/
private static void clearInputBuffer()
{
try {
int n = System.in.available();
while(n != 0) {
System.in.skip(n);
n = System.in.available();
}
}
catch(IOException ioe) { }
}
/**
* Bug in the java debug VM means that exception events are unreliable
* if we re-use the same thread over and over. So, whenever running user
* code results in an exception, this method is used to spawn a new thread.
*/
private static void newThread()
{
final Thread oldThread = mainThread;
// Then make a new one.
mainThread = new Thread("main") {
public void run()
{
try {
if(oldThread != null) {
oldThread.join();
}
}
catch(InterruptedException ie) { }
vmStarted();
// Execute the command
methodReturn = null;
exception = null;
try {
switch(execAction) {
case EXEC_SHELL:
{
// Execute a shell class.
methodReturn = null;
executedClass = null;
clearInputBuffer();
Class<?> c = currentLoader.loadClass(classToRun);
executedClass = c;
// Class c = cloader.loadClass(classToRun);
Method m = c.getMethod("run", new Class[0]);
try {
methodReturn = m.invoke(null, new Object[0]);
}
catch(InvocationTargetException ite) {
throw ite.getCause();
}
break;
}
case INSTANTIATE_CLASS:
{
// Instantiate a class using the default
// constructor
clearInputBuffer();
Class<?> c = currentLoader.loadClass(classToRun);
Constructor<?> cons = c.getDeclaredConstructor(new Class[0]);
cons.setAccessible(true);
try {
methodReturn = cons.newInstance((Object []) null);
}
catch (InvocationTargetException ite) {
throw ite.getCause();
}
break;
}
case INSTANTIATE_CLASS_ARGS:
{
// Instantiate a class using specified parameter
// types and arguments
clearInputBuffer();
Class<?> c = currentLoader.loadClass(classToRun);
Class<?> [] paramClasses = new Class[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
if (classLoader == null)
classLoader = currentLoader;
paramClasses[i] = Class.forName(parameterTypes[i], false, currentLoader);
}
Constructor<?> cons = c.getDeclaredConstructor(paramClasses);
cons.setAccessible(true);
try {
methodReturn = cons.newInstance(arguments);
}
catch (InvocationTargetException ite) {
throw ite.getCause();
}
break;
}
case TEST_SETUP:
methodReturn = runTestSetUp(classToRun);
break;
case TEST_RUN:
methodReturn = runTestMethod(classToRun, methodToRun);
break;
case DISPOSE_WINDOWS:
disposeWindows();
break;
case LOAD_INIT_CLASS:
try {
methodReturn = loadAndInitClass(classToRun);
}
catch(Throwable cnfe) {
methodReturn = null;
}
break;
case EXIT_VM:
System.exit(0);
default:
}
}
catch(Throwable t) {
// record that an exception occurred
recordException(t);
}
finally {
// Set execAction to EXIT_VM, so if the main bluej process has died,
// this vm will exit also.
execAction = EXIT_VM;
newThread();
}
}
};
mainThread.start();
}
/**
* Record that an exception occurred, as well as printing a filtered stack trace.
* @param t the exception which was caught
*/
private static void recordException(Throwable t)
{
// record that an exception occurred
exception = t;
// print a filtered stack trace to System.err
StackTraceElement [] stackTrace = t.getStackTrace();
int i;
for(i = 0; i < stackTrace.length; i++) {
if(stackTrace[i].getClassName().startsWith("__SHELL"))
break;
}
// Now scan backwards to strip out reflection stuff
for (int j = i - 1; j >= 0; j--)
{
// look for highest stack frame that is not a bluej call or
// a reflection call
if (!stackTrace[j].getClassName().startsWith("bluej.")
&& !stackTrace[j].getClassName().startsWith("java.lang.reflect.")
&& !stackTrace[j].getClassName().startsWith("sun.reflect."))
{
i = j + 1;
break;
}
}
StackTraceElement [] newStackTrace = new StackTraceElement[i];
System.arraycopy(stackTrace, 0, newStackTrace, 0, i);
t.setStackTrace(newStackTrace);
t = org.webcat.exceptiondoctor.ExceptionDoctor.addExplanation(t);
t.printStackTrace();
}
/**
* Gets an object in the scope. Used by greenfoot.
*
* @param instanceName The name of the object
* @return The object
*/
public static Object getObject(String instanceName)
{
BJMap<String,Object> m = getScope(scopeId);
Object rval = null;
try {
synchronized (m) {
rval = m.get(instanceName);
// Sometimes the object isn't available yet - the worker thread
// hasn't stored it in the map yet. In that case we'll wait to
// be notified that an object has been stored.
if (rval == null) {
m.wait();
rval = m.get(instanceName);
}
}
}
catch (InterruptedException ie) {}
return rval;
}
/**
* Get the name-to-object map for the current package scope.
* Access to the map must be synchronized.
*/
public static BJMap<String,Object> getObjectMap()
{
return getScope(scopeId);
}
/**
* Get the current class loader used to load user classes.
*
* @return The current class loader
*/
public static ClassLoader getCurrentClassLoader()
{
return currentLoader;
}
/**
* Set the current class loader, to be used for loading user classes.
*
* @param newLoader The new class loader
*/
public static void setClassLoader(ClassLoader newLoader)
{
currentLoader = newLoader;
}
}