Debug Framework Tutorials
For a list of all debug related tutorials see Debug Framework Tutorials Overview.
Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.
Debug Framework briefly explained
As each debugger is different, the Debug Framework uses a generic model to interact with the debugger. The main entry point into this model is the DebugTarget. It consists of a single Process which may contain one to many threads. Both can be real (eg. for an external debugger) or virtual implementations if our debugger runs in the eclipse context.
When threads are suspended they may provide StackFrames. These represent executable units typically with an individual scope. In Java debugging each instance on the call stack is represented as a frame. StackFrames also keep track of variables.
As debugging is triggered by UI events (resume, step into, ...) all interactions between the debugger and the model should be handled in a separate event processing thread. Eclipse uses a "fire-and-forget" policy here. Eg when someone pushes the resume button, IThread.resume() is called, which should return immediately. In the background the event processor should then take means to inform the debugger of the resume request. In case the debugger changes its state to resumed, it will inform our model which will update its internal representation and then send an event to the framework.
The decoupling using events is a bit harder to implement, but necessary to keep the UI responsive.
For a detailed description of the debug framework have a look at
- How to Write an Eclipse Debugger
- have a look at the slides and provided sample code from The Eclipse Debug Framework, a session from EclipseCon 2005
- read a report of the EclipseCon session
- read the platform docs of your eclipse help (Platform Plug-In Developer Guide / Programmer's Guide / Program debug and launch support / Debugging a program)
You should definitely read some of the above to fully understand what we are doing in the upcoming tutorials.
As we need to provide lots of classes now I advise to download the project from github. Here we will only look at the relevant parts of the implementation.
Step 1: The event dispatcher
For all debugging stuff we will use a new Plug-In called com.codeandme.debugger.textinterpreter.debugger.
Communication between the debugger and the model (see the diagram above) is handled by our EventDispatchJob. It accepts incoming events with addEvent(), and delivers them in the handleEvent() method. Events itself can either be IModelRequests (for interactions in eclipse that should trigger an action in the debugger) or IDebuggerEvents (when the debugger changes its state, hits breakpoints, ...). Both are just marker interfaces.
The model and the debugger need to implement the IEventProcessor interface to accept events.
Step 2: The debugger
The TextDebugger has to implement the IDebugger interface from our interpreter and the IEventProcessor interface to handle events received from the EventDispatcher. The implementation refers to the left hand side of the block diagram we have seen above. Let's have a look at some relevant code parts:
public class TextDebugger implements IDebugger, IEventProcessor { @Override public void loaded() { fireEvent(new DebuggerStartedEvent()); } @Override public void resumed() { fireEvent(new ResumedEvent()); } @Override public void terminated() { fireEvent(new TerminatedEvent()); } @Override public void handleEvent(final IDebugEvent event) { if (event instanceof ResumeRequest) fInterpreter.resume(); } private void fireEvent(final IDebugEvent event) { fDispatcher.addEvent(event); } }We can see that events are fired whenever our interpreter changes its state (loaded(), resumed(), terminated()). The debugger simply passes them on to the dispatcher. The loaded event is a special kind of a resumed event: it tells our model that a new interpreter has been set up and that source has been loaded for processing. In future the model will set breakpoints before the interpreter starts executing the code.
Whenever the model sends events to the debugger, handleEvent() is called by the dispatcher. In our case we simply allow to resume (which is needed after a loaded event, that automatically suspends the interpreter).
Step 3: The model
Lets start with the TextDebugTarget, as it is our main entry point into the model. Again we will look at some relevant parts only.
public class TextDebugTarget extends TextDebugElement implements IDebugTarget, IEventProcessor { public enum State { NOT_STARTED, SUSPENDED, RESUMED, TERMINATED, DISCONNECTED }; public TextDebugTarget(final ILaunch launch, IFile file) { super(null); fLaunch = launch; fFile = file; fireCreationEvent(); // create a process fProcess = new TextProcess(this); fProcess.fireCreationEvent(); } void fireModelEvent(final IDebugEvent event) { fDispatcher.addEvent(event); } @Override public void handleEvent(final IDebugEvent event) { if (!isDisconnected()) { if (event instanceof DebuggerStartedEvent) { // create debug thread TextThread thread = new TextThread(this); fThreads.add(thread); thread.fireCreationEvent(); // debugger got started and waits in suspended mode fState = State.SUSPENDED; // inform eclipse of suspended state fireSuspendEvent(DebugEvent.CLIENT_REQUEST); } else if (event instanceof ResumedEvent) { fState = State.RESUMED; // inform eclipse of resumed state fireResumeEvent(DebugEvent.UNSPECIFIED); } else if (event instanceof TerminatedEvent) { // debugger is terminated fState = State.TERMINATED; // we do not need our dispatcher anymore fDispatcher.terminate(); // inform eclipse of terminated state fireTerminateEvent(); } } } // ************************************************************ // ISuspendResume // ************************************************************ @Override public boolean canResume() { return isSuspended(); } @Override public boolean isSuspended() { return (fState == State.SUSPENDED); } @Override public void resume() { // resume request from eclipse // send resume request to debugger fireModelEvent(new ResumeRequest()); }Eclipse provides a nice base class for all model parts: DebugElement. We add our own base class TextDebugElement on top of that. Right now it does nothing, but we will need it for the next tutorial.
The first thing we see is, that the model needs to track the state of the debugger (line 4, 35, 41, ...). This is necessary to enable/disable debugging options on the toolbar.
In the constructor we fire a creation event (line 13). This event is sent to the Eclipse Debug Framework and might trigger UI interactions (like the switch to the debug perspective). After that we create a TextProcess and again fire a creation event for that. We will see this pattern all the time when interacting with the framework. Whenever we think that something relevant has changed, we need to inform the framework of that change by sending an event. There is no other means of interaction available for our model.
handleEvent() is the counterpart to TextDebugger.handleEvent(). On a DebuggerStartedEvent we create a new Thread, adjust our state model and tell Eclipse that our debugger is in suspended mode. Eclipse will then enable the resume toolbar button for us. When the TerminateEvent arrives we need to shut down our scheduler, otherwise it would run forever.
Before Eclipse really enables the resume button (as described above) it queries our model calling canResume(). We only allow Eclipse to resume when the debugger is currently suspended. resume() then triggers a new event that is passed to the Dispatcher and finally handled by the debugger.
TextDebugTarget needs to implement lots of interfaces for breakpoint handling, terminate/resume/suspend support and many things more. Right now these implementations will simply throw exceptions as they are not implemented yet. Within the next tutorials we will improve them step by step.
TextProcess and TextThread are almost empty, so we do not need to investigate them right now.
Step 4: Add debugging support to launchers
When we created our launch extensions, we just allowed run modes. To add debug launch targets open com.codeandme.debugger.textinterpreter.ui/plugin.xml and switch to Extensions.
- First go to the launchConfigurationType and set modes to "run,debug".
- Next locate our launchConfigurationTabGroup and add a new launchMode. There set mode to "debug" and perspective to "org.eclipse.debug.ui.DebugPerspective".
- Finally open the launchShortcut and set modes to "run,debug".
Open TextLaunchDelegate.java. Locate the launch method at the very end of the file and replace it with following code:
private void launch(final IFile file, final ILaunchConfiguration configuration, final String mode, final ILaunch launch, final IProgressMonitor monitor) { // create new interpreter TextInterpreter interpreter = new TextInterpreter(); try { if (ILaunchManager.DEBUG_MODE.equals(mode)) { // create debugger TextDebugger debugger = new TextDebugger(interpreter); interpreter.setDebugger(debugger); // create debugTarget TextDebugTarget debugTarget = new TextDebugTarget(launch, file); // create & launch dispatcher EventDispatchJob dispatcher = new EventDispatchJob(debugTarget, debugger); // attach dispatcher to debugger & debugTarget debugger.setEventDispatcher(dispatcher); debugTarget.setEventDispatcher(dispatcher); // add debug target to launch launch.addDebugTarget(debugTarget); } interpreter.setCode(toString(file)); interpreter.schedule(); } catch (Exception e) { // TODO handle this exception (but for now, at least know it // happened) throw new RuntimeException(e); } }The basic components we need are:
- a debugger implementation (lines 9-10)
The debugger is attached to our interpreter and handles all kinds of debug interactions on client side. - A DebugTarget (line 13)
The DebugTarget is the representative of our debugger on Eclipse side. Whenever the Eclipse Debug Framework needs to interact with our debugger, it calls the DebugTarget to process the request. - An asynchronous event dispatcher (16-17)
As Eclipse debug requests (like resume, step over, ...) originate from the UI, they need to be handled asynchronously. The event dispatcher handles these requests in its own thread.
To track how the debugger works I left some trace messages inside the code. To enable trace output, open your launch configuration, switch to the Tracing tab and select Enable tracing. Then activate com.codeandme.debugger.textinterpreter.debugger and on the right hand side the debug node.
When you run the debugger it will suspend after setting up the interpreter and debugger. When you hit the resume button, you should end up with following console output:
Debugger : new DebuggerStartedEvent Dispatcher: [+] DebuggerStartedEvent Dispatcher: debugger -> DebuggerStartedEvent -> model Model : process DebuggerStartedEvent Model : new ResumeRequest Dispatcher: [+] ResumeRequest Dispatcher: debugger <- ResumeRequest <- model Debugger : process ResumeRequest Debugger : new ResumedEvent Dispatcher: [+] ResumedEvent Dispatcher: debugger -> ResumedEvent -> model Model : process ResumedEvent Hello World name = Christian Christian Debugger : new TerminatedEvent Dispatcher: [+] TerminatedEvent Dispatcher: debugger -> TerminatedEvent -> model Model : process TerminatedEventAfter we received the DebuggerStartedEvent, our debugger suspends and waits for a ResumeRequest triggered by eclipse. So make sure you hit the resume button to continue execution.
Not much of a debugger so far, but all the basic components are in place. In the following tutorial we will start to fill our model with some spice.
No comments:
Post a Comment