Finally you have made it: all preparations are done and we are ready to start with our debugger. By the end of this tutorial we will be able to run a simple debug session:
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
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".
Step 5: Adding launch implementation
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.
Step 6: Running the debugger
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 TerminatedEvent
After 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.