Thursday, November 14, 2013

Debugger 4: Stepping, suspending and other actions

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.

No comments:

Post a Comment