package greenfoot.vmcomm;
import bluej.debugger.VarDisplayInfo;
import bluej.debugger.gentype.GenTypeClass;
import bluej.debugger.gentype.JavaType;
import greenfoot.core.PickActorHelper;
import greenfoot.core.Simulation;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import greenfoot.core.ProjectManager;
import greenfoot.core.WorldHandler;
import greenfoot.guifx.GreenfootStage;
import greenfoot.platforms.ide.WorldHandlerDelegateIDE;
import greenfoot.record.GreenfootRecorder;
import greenfoot.util.DebugUtil;
import javafx.application.Platform;
import bluej.debugger.Debugger;
import bluej.debugger.DebuggerClass;
import bluej.debugger.DebuggerEvent;
import bluej.debugger.DebuggerField;
import bluej.debugger.DebuggerListener;
import bluej.debugger.DebuggerObject;
import bluej.debugger.DebuggerThread;
import bluej.debugger.SourceLocation;
import bluej.debugmgr.Invoker;
import bluej.debugmgr.NamedValue;
import bluej.debugmgr.ValueCollection;
import bluej.debugmgr.objectbench.ObjectBenchEvent;
import bluej.debugmgr.objectbench.ObjectBenchInterface;
import bluej.debugmgr.objectbench.ObjectBenchListener;
import bluej.pkgmgr.Project;
import bluej.utility.Debug;
import bluej.utility.JavaNames;
import threadchecker.OnThread;
import threadchecker.Tag;
| A class that does several things:
|
| <p>Firstly, it listens for the debugger terminating the Greenfoot VM, and relaunches Greenfoot.
|
| <p>Secondly, it tries to make sure that the debugger never stops the code
| entirely outside the user's code (i.e. there should always be some user code
| somewhere in the call stack)
|
| @author Neil Brown
|
public class GreenfootDebugHandler
implements DebuggerListener, ObjectBenchInterface, ValueCollection{
private static final String SIMULATION_CLASS = Simulation.class.getName();
private static final String[] INVOKE_METHODS = {Simulation.ACT_WORLD, Simulation.ACT_ACTOR,
Simulation.NEW_INSTANCE, Simulation.RUN_QUEUED_TASKS, Simulation.WORLD_STARTED, Simulation.WORLD_STOPPED
};
private static final String SIMULATION_INVOKE_KEY = SIMULATION_CLASS + "INTERNAL";
private static final String PAUSED_METHOD = Simulation.PAUSED;
private static final String SIMULATION_THREAD_PAUSED_KEY = "SIMULATION_THREAD_PAUSED";
private static final String SIMULATION_THREAD_RESUMED_KEY = "SIMULATION_THREAD_RESUMED";
private static final String SIMULATION_THREAD_RUN_KEY = "SIMULATION_THREAD_RUN";
private static final String WORLD_HANDLER_CLASS = WorldHandler.class.getName();
private static final String WORLD_CHANGED_KEY = "WORLD_CHANGED";
private static final String WORLD_INITIALISING_KEY = "WORLD_INITIALISING";
private static final String WORLD_INSTANTIATION_ERROR_KEY = "WORLD_INSTANTIATION_ERROR";
private static final String NAME_ACTOR_CLASS = WorldHandlerDelegateIDE.class.getName();
private static final String NAME_ACTOR_KEY = "NAME_ACTOR";
private static final String PICK_HELPER_CLASS = PickActorHelper.class.getName();
private static final String PICK_HELPER_KEY = "PICK_HELPER_PICKED";
private PickListener pickListener;
private Project project;
private DebuggerThread simulationThread;
private DebuggerClass simulationClass;
private GreenfootRecorder greenfootRecorder;
private SimulationStateListener simulationListener;
private Map<String,GreenfootObject> objectBench = new HashMap<>();
private List<ObjectBenchListener> benchListeners = new ArrayList<>();
private VMCommsMain vmComms;
private boolean hasLaunched = false;
| Constructor for GreenfootDebugHandler.
|
@OnThread(Tag.FXPlatform)
private GreenfootDebugHandler(Project project) throws IOException
{
this.project = project;
greenfootRecorder = new GreenfootRecorder();
vmComms = new VMCommsMain(project);
}
| This is the publicly-visible way to add a debugger listener for a particular project.
|
@OnThread(Tag.FXPlatform)
public static void addDebuggerListener(Project project) throws IOException
{
project.getExecControls().setRestrictedClasses(DebugUtil.restrictedClassesAsNames());
GreenfootDebugHandler handler = new GreenfootDebugHandler(project);
if (project.getDebugger().addDebuggerListener(handler) == Debugger.IDLE)
{
handler.launch(project.getDebugger());
}
GreenfootStage.makeStage(project, handler).show();
}
| Set the listener which will be called when a pick request completes.
| @param pickListener Will be called with the pickId and list of actors and world at that position.
|
public void setPickListener(PickListener pickListener)
{
this.pickListener = pickListener;
}
@OnThread(Tag.FXPlatform)
private void addRunResetBreakpoints(Debugger debugger)
{
try {
simulationClass = debugger.getClass(SIMULATION_CLASS, true).get();
setBreakpoint(debugger, SIMULATION_CLASS, "run", SIMULATION_THREAD_RUN_KEY);
setBreakpoint(debugger, SIMULATION_CLASS, PAUSED_METHOD, SIMULATION_THREAD_PAUSED_KEY);
setBreakpoint(debugger, SIMULATION_CLASS, "resumeRunning", SIMULATION_THREAD_RESUMED_KEY);
setBreakpoint(debugger, WORLD_HANDLER_CLASS, "setInitialisingWorld", WORLD_INITIALISING_KEY);
setBreakpoint(debugger, WORLD_HANDLER_CLASS, "worldChanged", WORLD_CHANGED_KEY);
setBreakpoint(debugger, WORLD_HANDLER_CLASS, "worldInstantiationError", WORLD_INSTANTIATION_ERROR_KEY);
setBreakpoint(debugger, NAME_ACTOR_CLASS, "nameActors", NAME_ACTOR_KEY);
setBreakpoint(debugger, PICK_HELPER_CLASS, "picked", PICK_HELPER_KEY);
}
catch (ClassNotFoundException cnfe) {
Debug.reportError("Simulation class could not be located. Possible installation problem.", cnfe);
}
}
| Sets a breakpoint in the given class and method, and identifies it by setting a
| breakpoint point property with breakpointKey mapped to "TRUE"
|*/
@OnThread(Tag.FXPlatform)
private void setBreakpoint(Debugger debugger, String className, String methodName, String breakpointKey)
{
Map<String, String> breakpointProperties = new HashMap<String, String>();
|
|breakpointProperties.put(breakpointKey, "TRUE");
breakpointProperties.put(Debugger.PERSIST_BREAKPOINT_PROPERTY, "TRUE");
debugger.toggleBreakpoint(className, methodName, true, breakpointProperties);
}
@OnThread(Tag.Any)
private boolean isSimulationThread(DebuggerThread dt)
{
return dt != null && simulationThread != null && simulationThread.sameThread(dt);
|
|}
|
|/**
| Get the inter-VM communications channel.
|
public VMCommsMain getVmComms()
{
return vmComms;
}
| Get the temporary file used as the shared memory communication backing.
|
@OnThread(Tag.FXPlatform)
public File getShmFile()
{
return vmComms.getSharedFile();
}
| Get the size of the temporary file used as the shared memory communication backing.
|
@OnThread(Tag.FXPlatform)
public int getShmFileSize()
{
return vmComms.getSharedFileSize();
}
| Get the recorder instance which tracks actor creation.
|
public GreenfootRecorder getRecorder()
{
return greenfootRecorder;
}
| An early examination of the debugger event (gets called before processDebuggerEvent)
|
| This method is responsible for checking where the debugger has stopped (if it has),
| and deciding whether it should be run on for a bit until it reaches user code.
|
| This method does not actually run it on; see the comments on the scheduledTasks field
| at the top of the class for how it works.
|
@Override
public boolean examineDebuggerEvent(final DebuggerEvent e)
{
final Debugger debugger = (Debugger)e.getSource();
List<SourceLocation> stack = e.getThread().getStack();
boolean atBreakpoint = e.getID() == DebuggerEvent.THREAD_BREAKPOINT && e.getThread() != null && e.getBreakpointProperties() != null;
if (atBreakpoint && e.getBreakpointProperties().get(SIMULATION_THREAD_RUN_KEY) != null)
{
simulationThread = e.getThread();
Platform.runLater(() -> project.getExecControls().selectThread(simulationThread));
e.getThread().cont();
return true;
}
else if (atBreakpoint && e.getBreakpointProperties().get(SIMULATION_THREAD_RESUMED_KEY) != null)
{
if (simulationListener != null)
{
simulationListener.simulationStartedRunning();
}
e.getThread().cont();
return true;
}
else if (atBreakpoint && e.getBreakpointProperties().get(NAME_ACTOR_KEY) != null)
{
VarDisplayInfo varDisplayInfo = e.getThread().getLocalVariables(0).get(0);
greenfootRecorder.nameActors(fetchArray(varDisplayInfo.getFetchObject().get()));
e.getThread().cont();
return true;
}
else if (atBreakpoint && e.getBreakpointProperties().get(WORLD_INITIALISING_KEY) != null)
{
greenfootRecorder.clearCode(true);
e.getThread().cont();
return true;
}
else if (atBreakpoint && e.getBreakpointProperties().get(WORLD_INSTANTIATION_ERROR_KEY) != null)
{
simulationListener.worldInstantiationError();
e.getThread().cont();
return true;
}
else if (atBreakpoint && e.getBreakpointProperties().get(WORLD_CHANGED_KEY) != null)
{
List<DebuggerField> fields = e.getThread().getCurrentObject(0).getFields();
DebuggerField worldField = fields.stream().filter(f -> f.getName().equals("world")).findFirst().orElse(null);
if (worldField != null)
{
DebuggerObject worldValue = worldField.getValueObject();
greenfootRecorder.setWorld(worldValue);
}
e.getThread().cont();
return true;
}
else if (atBreakpoint && e.getBreakpointProperties().get(PICK_HELPER_KEY) != null)
{
List<DebuggerField> fields = e.getThread().getCurrentObject(0).getFields();
DebuggerField actorPicksField = fields.stream().filter(f -> f.getName().equals("actorPicks")).findFirst().orElse(null);
DebuggerField worldPickField = fields.stream().filter(f -> f.getName().equals("worldPick")).findFirst().orElse(null);
DebuggerField pickIdField = fields.stream().filter(f -> f.getName().equals("pickId")).findFirst().orElse(null);
if (actorPicksField != null && worldPickField != null && pickIdField != null)
{
DebuggerObject actorPicksValue = actorPicksField.getValueObject();
DebuggerObject worldPickValue = worldPickField.getValueObject();
int pickIdValue = Integer.parseInt(pickIdField.getValueString());
if (actorPicksValue != null && actorPicksValue.isArray() && worldPickValue != null)
{
List<DebuggerObject> picksElements = fetchArray(actorPicksValue);
Platform.runLater(() -> {
if (pickListener != null)
{
pickListener.picked(pickIdValue, picksElements, worldPickValue);
}
});
}
}
e.getThread().cont();
return true;
}
else if (e.isHalt() && isSimulationThread(e.getThread()))
{
if (atBreakpoint && e.getBreakpointProperties().get(SIMULATION_THREAD_PAUSED_KEY) != null)
{
removeSpecialBreakpoints(debugger);
if (simulationListener != null)
{
simulationListener.simulationPaused();
}
e.getThread().cont();
return true;
}
else if (insideUserCode(stack))
{
debugger.removeBreakpointsForClass(SIMULATION_CLASS);
if (atBreakpoint && e.getBreakpointProperties().get(SIMULATION_INVOKE_KEY) != null) {
e.getThread().stepInto();
return true;
}
else if (inInvokeMethods(stack, 0)) {
runToInternalBreakpoint(debugger, e.getThread());
return true;
}
}
else {
if (inPauseMethod(stack)) {
e.getThread().cont();
}
else {
runToInternalBreakpoint(debugger, e.getThread());
}
return true;
}
}
return false;
}
| Fetches all the objects in a debug VM array into
| a server VM list of debug objects (the array elements).
| @param arrayValue
| @return
|
@OnThread(Tag.Any)
@SuppressWarnings("threadchecker")
private List fetchArray(DebuggerObject arrayValue)
{
List<DebuggerObject> elements = new ArrayList<>(arrayValue.getElementCount());
for (int i = 0; i < arrayValue.getElementCount(); i++)
{
elements.add(arrayValue.getElementObject(i));
}
return elements;
}
| Processes a debugger event. This is called after examineDebuggerEvent, with a second
| parameter that effectively corresponds to the return result of examineDebuggerEvent.
|
| <p>Thus, if the parameter is true, we look for a scheduled task to run.
|
| <p>We call threadHalted if necessary.
|
@Override
public void processDebuggerEvent(final DebuggerEvent e, boolean skipUpdate)
{
if (e.getNewState() == Debugger.NOTREADY)
{
hasLaunched = false;
vmComms.vmTerminated();
if (simulationListener != null)
{
simulationListener.simulationVMTerminated();
}
}
if (e.getNewState() == Debugger.IDLE && !hasLaunched)
{
launch((Debugger) e.getSource());
}
if (!skipUpdate)
{
if (e.isHalt() && isSimulationThread(e.getThread())&& simulationListener != null)
{
simulationListener.simulationDebugHalted();
}
else if (e.getID() == DebuggerEvent.THREAD_CONTINUE && isSimulationThread(e.getThread()) && simulationListener != null)
{
simulationListener.simulationDebugResumed();
}
}
}
| Launches Greenfoot on the debug VM. Only call this once (check the hasLaunched flag before calling)
| @param debugger The debugger for the project.
|
private void launch(Debugger debugger)
{
if (! ProjectManager.checkLaunchFailed())
{
hasLaunched = true;
Platform.runLater(() -> {
objectBench.clear();
addRunResetBreakpoints(debugger);
ProjectManager.instance().openGreenfoot(project, GreenfootDebugHandler.this);
});
}
}
| Runs the debugger on until it hits the special invoke-act breakpoints that occur
| just before user code might be encountered. This method doesn't actually check if you're
| thereabouts already, so it should be only called once you've checked that you actually
| want to run onwards.
|
| Returns a task that will run them onwards, which can be scheduled as you like
|
private void runToInternalBreakpoint(final Debugger debugger, final DebuggerThread thread)
{
setSpecialBreakpoints(debugger);
thread.cont();
}
| Works out if we are currently in a call to the World or Actor act() methods
| by looking in the call stack for them. Strictly speaking, we might not be
| truly inside the user code: it might be we are about to enter or have just
| left the act() method. It is only valid to call this for the simulation
| thread.
|
private static boolean insideUserCode(List<SourceLocation> stack)
{
for (int i = 0; i < stack.size();i++) {
if (inInvokeMethods(stack, i)) {
return true;
}
}
return false;
}
| Works out if the specified frame in the call-stack is in one of the special invoke-act
| methods that call the World and Actor's act() methods or the method that runs
| other user code on the simulation thread
|
private static boolean inInvokeMethods(List<SourceLocation> stack, int frame)
{
if (frame < stack.size()) {
String className = stack.get(frame).getClassName();
if (className.equals(SIMULATION_CLASS)) {
String methodName = stack.get(frame).getMethodName();
for (String actMethod : INVOKE_METHODS) {
if (actMethod.equals(methodName)) {
return true;
}
}
}
else if (JavaNames.getBase(className).startsWith(Invoker.SHELLNAME)) {
return true;
}
}
return false;
}
| Works out if they are currently paused by looking at the call stack
| while they are suspended.
|
private static boolean inPauseMethod(List<SourceLocation> stack)
{
for (SourceLocation loc : stack) {
if (loc.getClassName().equals(SIMULATION_CLASS) && loc.getMethodName().equals(PAUSED_METHOD)) {
return true;
}
}
return false;
}
| Sets breakpoints in the special invoke-act methods that call the World and Actor's
| act() methods, and the method that constructs new objects, and the method called when
| the simulation will pause. These breakpoints will thus be encountered immediately before control
| would descend into the World and Actor's act() methods or other tasks (i.e. potential user code),
| or if the simulation is going to wait for the user to click the controls (e.g. end of an
| Act, or because the simulation is now going to be Paused).
|
private void setSpecialBreakpoints(final Debugger debugger)
{
for (String method : INVOKE_METHODS) {
String err = debugger.toggleBreakpoint(simulationClass, method, true, Collections.singletonMap(SIMULATION_INVOKE_KEY, "yes"));
if (err != null) {
Debug.reportError("Problem setting special breakpoint: " + err);
}
}
}
| Removes the breakpoints added by setSpecialBreakpoints
|
private void removeSpecialBreakpoints(Debugger debugger)
{
for (String method : INVOKE_METHODS)
{
debugger.toggleBreakpoint(simulationClass, method, false, Collections.singletonMap(SIMULATION_INVOKE_KEY, "yes"));
}
}
public void setSimulationListener(SimulationStateListener simulationListener)
{
this.simulationListener = simulationListener;
}
| Halts the simulation thread.
|
public void haltSimulationThread()
{
if (simulationThread != null)
{
simulationThread.halt();
}
}
@Override
public String addObject(DebuggerObject object, GenTypeClass type,
String name)
{
while (objectBench.get(name) != null){
name += "_";
}
objectBench.put(name, new GreenfootObject(object, type, name));
return name;
}
| Ensure that an object is "on the bench" (known to the debugger and invoker).
|* @param object The object to put (or find) on the bench
* @param type The type of the object
* @return The wrapped bench object
*/
@OnThread(Tag.FXPlatform)
|
|public NamedValue ensureObjectOnBench(DebuggerObject object, GenTypeClass type)
|
|{
|
|GreenfootObject selectedObject = null;
|
|for (GreenfootObject benchObj : objectBench.values())
|
|{
|
|if (benchObj.getDebuggerObject().equals(object))
|
|{
|
|selectedObject = benchObj;
|
|break;
|
|}
|
|}
|
|// If the object isn't on the bench yet, we must add it now:
|
|if (selectedObject == null)
|
|{
|
|String name = project.getDebugger().guessNewName(object);
|
|project.getDebugger().addObject(project.getPackage("").getId(), name, object);
GreenfootObject newObj = new GreenfootObject(object, type, name);
objectBench.put(name, newObj);
selectedObject = newObj;
|
|}
|
|return selectedObject;
|
|}
|
|/**
| Add an object to the "object bench" and fire an event to listeners notifying that the new object has
|* been selected.
*
* @param object The object to add
* @param type The type of the object
* @return The name of the object as it is known to the debugger
@OnThread(Tag.FXPlatform)
public String addSelectedObject(DebuggerObject object, GenTypeClass type)
{
NamedValue selectedObject = ensureObjectOnBench(object, type);
for (ObjectBenchListener l : benchListeners)
{
l.objectEvent(new ObjectBenchEvent(this, ObjectBenchEvent.OBJECT_SELECTED, selectedObject));
}
return selectedObject.getName();
}
@Override
public void addObjectBenchListener(ObjectBenchListener l)
{
benchListeners.add(l);
}
@Override
public void removeObjectBenchListener(ObjectBenchListener l)
{
benchListeners.remove(l);
}
@Override
public boolean hasObject(String name)
{
return objectBench.get(name) != null;
}
@Override
public NamedValue getNamedValue(String name)
{
return objectBench.get(name);
}
@Override
public Iterator extends NamedValue> getValueIterator()
{
return objectBench.values().iterator();
}
| An object on the "object bench". In Greenfoot this is largely virtualised.
|*/
private static class GreenfootObject implements NamedValue
{
private GenTypeClass type;
private String name;
private DebuggerObject debuggerObject;
|
|/**
| Construct a GreenfootObject with the given name and type.
|
public GreenfootObject(DebuggerObject object, GenTypeClass type, String name)
{
this.type = type;
this.name = name;
this.debuggerObject = object;
}
@Override
public JavaType getGenType()
{
return type;
}
@Override
public String getName()
{
return name;
}
| Get the DebuggerObject that this GreenfootObject wraps.
|
public DebuggerObject getDebuggerObject()
{
return debuggerObject;
}
@Override
public boolean isFinal()
{
return false;
}
@Override
public boolean isInitialized()
{
return true;
}
}
| A listener for results of pick requests.
|
public static interface PickListener
{
@OnThread(Tag.Any)
public void picked(int pickId, List<DebuggerObject> actors, DebuggerObject world);
}
| A listener to the simulation's state
|
public static interface SimulationStateListener
{
| Called when the simulation starts running
|
@OnThread(Tag.Any)
public void simulationStartedRunning();
| Called when the simulation has been paused in a normal manner
| (i.e. either user hit pause, or called Greenfoot.stop)
|
@OnThread(Tag.Any)
public void simulationPaused();
| Called when the simulation thread has hit a (user) breakpoint
|
@OnThread(Tag.Any)
public void simulationDebugHalted();
| Called when the simulation thread has resumed from a (user) breakpoint
|
@OnThread(Tag.Any)
public void simulationDebugResumed();
| Called when there is an error while instantiating the world.
|
@OnThread(Tag.Any)
public void worldInstantiationError();
| Called when the debug VM has just terminated
| (but not yet restarted)
|
@OnThread(Tag.Any)
public void simulationVMTerminated();
}
| Set the simulation thread going if it's suspended: The user has clicked reset (or a reset
| has otherwise been issued, eg after successful compile).
|
public void simulationThreadResumeOnResetClick()
{
if (simulationThread != null && simulationThread.isSuspended())
{
simulationThread.cont();
}
objectBench.clear();
}
}
top,
use,
map,
class GreenfootDebugHandler
. GreenfootDebugHandler
. addDebuggerListener
. setPickListener
. addRunResetBreakpoints
. getVmComms
. getShmFile
. getShmFileSize
. getRecorder
. examineDebuggerEvent
. fetchArray
. processDebuggerEvent
. launch
. runToInternalBreakpoint
. insideUserCode
. inInvokeMethods
. inPauseMethod
. setSpecialBreakpoints
. removeSpecialBreakpoints
. setSimulationListener
. haltSimulationThread
. addObject
. addSelectedObject
. addObjectBenchListener
. removeObjectBenchListener
. hasObject
. getNamedValue
. getValueIterator
. GreenfootObject
. getGenType
. getName
. getDebuggerObject
. isFinal
. isInitialized
top,
use,
map,
interface PickListener
. picked
top,
use,
map,
interface SimulationStateListener
. simulationStartedRunning
. simulationPaused
. simulationDebugHalted
. simulationDebugResumed
. worldInstantiationError
. simulationVMTerminated
. simulationThreadResumeOnResetClick
785 neLoCode
+ 95 LoComm