During the
previous tutorial we created the basic structure for our debugger. Today we will work on the UI integration part.
When our debugger is running, Eclipse provides different toolbar actions, depending on what selection is active in the
Debug view. There are four different types of actions:
- Terminate
- Suspend/Resume
- Disconnect
- Stepping
For each of these groups exists an interface (
ITerminate,
ISuspendResume,
IDisconnect and
IStep). Whenever the selected element implements one of these interfaces the corresponding toolbar actions become available. Enablement will be handled by interface methods like
canDisconnect(),
canResume() ...
In this tutorial we will implement all these features step by step.
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.
Step 1: Suspend/Resume
Our debugger is very simple. We do not have multiple threads, so suspend/resume functionality is the same for the DebugTarget, a Thread or a StackFrame (we will look into this in another tutorial).
As suspend/resume is already implemented, we simply refactor stuff and move the
ISuspendResume functionality to our base class
TextDebugElement.
public abstract class TextDebugElement extends DebugElement implements ISuspendResume {
public enum State {
NOT_STARTED, SUSPENDED, RESUMED, TERMINATED, DISCONNECTED
};
private State fState = State.NOT_STARTED;
@Override
public TextDebugTarget getDebugTarget() {
return (TextDebugTarget) super.getDebugTarget();
}
protected void setState(State state) {
// only the DebugTarget saves the correct state.
((TextDebugElement) getDebugTarget()).fState = state;
}
protected State getState() {
return ((TextDebugElement) getDebugTarget()).fState;
}
// ************************************************************
// ISuspendResume
// ************************************************************
@Override
public boolean canResume() {
return isSuspended();
}
@Override
public boolean canSuspend() {
// we cannot interrupt our debugger once it is running
return false;
}
@Override
public boolean isSuspended() {
return (getState() == State.SUSPENDED);
}
@Override
public void resume() {
// resume request from eclipse
// send resume request to debugger
getDebugTarget().fireModelEvent(new ResumeRequest());
}
@Override
public void suspend() throws DebugException {
throw new DebugException(new Status(IStatus.ERROR, "com.codeandme.textinterpreter.debugger", "suspend() not supported"));
}
}
Some minor changes are needed to the derived classes which are not shown here.
Any action that might be triggered from a toolbar item has a
canSomething() method that will be checked for button enablement (lines 26-35). Resuming is just a matter of sending a ResumeRequest to the debugger (line 47).
Moving to the base class allows any of our model implementations to use resuming. Besides we may remove some obsolete code from
TextDebugTarget and
TextThread.
When you run the debugger now try to select the
Text Thread element in the
Debug view. Previously we could not resume the thread, but only the
Text DebugTarget. Now we can resume both.
Step 2: Implement terminate and disconnect support
Again we add the functionality to the base class:
public abstract class TextDebugElement extends DebugElement implements ISuspendResume, IDisconnect, ITerminate {
// ************************************************************
// IDisconnect
// ************************************************************
@Override
public boolean canDisconnect() {
return canTerminate();
}
@Override
public void disconnect() {
// disconnect request from eclipse
// send disconnect request to debugger
getDebugTarget().fireModelEvent(new DisconnectRequest());
// debugger is detached, simulate terminate event
getDebugTarget().handleEvent(new TerminatedEvent());
}
@Override
public boolean isDisconnected() {
return isTerminated();
}
// ************************************************************
// ITerminate
// ************************************************************
@Override
public boolean canTerminate() {
return !isTerminated();
}
@Override
public boolean isTerminated() {
return (getState() == State.TERMINATED);
}
@Override
public void terminate() {
// terminate request from eclipse
// send terminate request to debugger
getDebugTarget().fireModelEvent(new TerminateRequest());
}
}
While
terminate() will immediately stop the debug session (along with the interpreter),
disconnect() will only detach the eclipse debugger, but resume the interpreter. Therefore we need to extend
TextDebugger.handleEvent():
public void handleEvent(final IDebugEvent event) {
if (event instanceof ResumeRequest)
fInterpreter.resume();
else if (event instanceof TerminateRequest)
fInterpreter.terminate();
else if (event instanceof DisconnectRequest) {
fInterpreter.setDebugger(null);
fInterpreter.resume();
}
}
On a disconnect we remove the debugger instance and resume. This allows our interpreter to continue execution without triggering further debug events.
Whether disconnect support shall be implemented or not depends on the debugger type. The default Java debugger in Eclipse does not provide disconnect support.
Step 3: Stepping support
Now for the interesting stuff: implement stepping.
Our fictional language is so simple, that it cannot support
step into or
step return. So what remains is
step over. Functionality for all three step types would be very similar anyhow. The only difference is made in your debugger implementation that needs to know when to suspend next. As before we implement the functionality in the base class.
public abstract class TextDebugElement extends DebugElement implements ISuspendResume, IDisconnect, ITerminate, IStep {
public enum State {
NOT_STARTED, SUSPENDED, RESUMED, TERMINATED, DISCONNECTED, STEPPING
};
// ************************************************************
// IStep
// ************************************************************
@Override
public boolean canStepInto() {
return false;
}
@Override
public boolean canStepOver() {
return isSuspended();
}
@Override
public boolean canStepReturn() {
return false;
}
@Override
public boolean isStepping() {
return (getState() == State.STEPPING);
}
@Override
public void stepInto() throws DebugException {
throw new DebugException(new Status(IStatus.ERROR, "com.codeandme.textinterpreter.debugger", "stepInto() not supported"));
}
@Override
public void stepOver() {
// stepOver request from eclipse
// send stepOver request to debugger
getDebugTarget().fireModelEvent(new ResumeRequest(ResumeRequest.STEP_OVER));
}
@Override
public void stepReturn() throws DebugException {
throw new DebugException(new Status(IStatus.ERROR, "com.codeandme.textinterpreter.debugger", "stepReturn() not supported"));
}
}
This will enable the
Step over toolbar button when the debugger is suspended.
The debug framework in Eclipse distinguishes between resuming and stepping. Therefore we added a new debugger state:
STEPPING.
Next step is to update the
TextDebugger class to keep track of the resume type (continue, step over, step into, step return):
public class TextDebugger implements IDebugger, IEventProcessor {
private boolean fIsStepping = false;
@Override
public void resumed() {
fireEvent(new ResumedEvent(fIsStepping ? ResumedEvent.STEPPING : ResumedEvent.CONTINUE));
}
@Override
public boolean isBreakpoint(final int lineNumber) {
return fIsStepping;
}
@Override
public void handleEvent(final IDebugEvent event) {
if (Activator.getDefault().isDebugging())
System.out.println("Debugger : process " + event);
if (event instanceof ResumeRequest) {
fIsStepping = (((ResumeRequest) event).getType() == ResumeRequest.STEP_OVER);
fInterpreter.resume();
} else if (event instanceof TerminateRequest)
fInterpreter.terminate();
else if (event instanceof DisconnectRequest) {
fInterpreter.setDebugger(null);
fInterpreter.resume();
}
}
}
Currently
isBreakpoint() is called for each executed line. We use the
fIsStepping marker to break after a step over event.
Finally the
TextDebugTarget needs to fire an adequate event for the debug framework:
public class TextDebugTarget extends TextDebugElement implements IDebugTarget, IEventProcessor {
@Override
public void handleEvent(final IDebugEvent event) {
if (!isDisconnected()) {
[...]
} else if (event instanceof ResumedEvent) {
if (((ResumedEvent) event).getType() == ResumedEvent.STEPPING) {
setState(State.STEPPING);
fireResumeEvent(DebugEvent.STEP_OVER);
} else {
setState(State.RESUMED);
fireResumeEvent(DebugEvent.UNSPECIFIED);
}
}
[...]
}
}
}
By now you should be able to run the debugger in single step mode. To verify results watch the sysout output in the console.