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.
  1. A fictional interpreter
  2. The launch framework
  3. A tale of debuggers, processes and threads
  4. Stepping, suspending and other actions
  5. UI breakpoint integration
  6. Debugger breakpoint integration
  7. Display current source code - source lookup
  8. Run to line support
  9. Displaying variables
  10. Displaying memory areas
Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly.

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.
package com.codeandme.textinterpreter.debugger.model;

public abstract class TextDebugElement extends DebugElement implements ISuspendResume, IDisconnect, ITerminate, IStep {

 // FIXME for full source code see http://codeandme.googlecode.com/svn/trunk/blog/debugger/04/com.codeandme.textinterpreter.debugger/src/com/codeandme/textinterpreter/debugger/model/TextDebugElement.java

 public enum State {
  NOT_STARTED, SUSPENDED, RESUMED, TERMINATED, STEPPING
 };

 private State mState = State.NOT_STARTED;

 protected void setState(State state) {
  // only the DebugTarget saves the correct state.
  ((TextDebugElement) getDebugTarget()).mState = state;
 }

 protected State getState() {
  return ((TextDebugElement) getDebugTarget()).mState;
 }

 // ************************************************************
 // 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(ResumeRequest.CONTINUE));
 }

 @Override
 public void suspend() throws DebugException {
  throw new DebugException(new Status(IStatus.ERROR, "com.codeandme.textinterpreter.debugger", "suspend() not supported"));
 }
}

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.

Step 2: Implement terminate and disconnect support

Again we add the functionality to the base class:
package com.codeandme.textinterpreter.debugger.model;

public abstract class TextDebugElement extends DebugElement implements ISuspendResume, IDisconnect, ITerminate, IStep {

 // FIXME for full source code see http://codeandme.googlecode.com/svn/trunk/blog/debugger/04/com.codeandme.textinterpreter.debugger/src/com/codeandme/textinterpreter/debugger/model/TextDebugElement.java

 // ************************************************************
 // 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():
 @Override
 public void handleEvent(final IDebugEvent event) {
  System.out.println("Debugger.handleEvent() " + event);

  if (event instanceof ResumeRequest) {
   mIsStepping = (((ResumeRequest) event).getType() == ResumeRequest.STEP_OVER);
   mInterpreter.resume();
  }

  else if (event instanceof TerminateRequest)
   mInterpreter.terminate();

  else if (event instanceof DisconnectRequest) {
   mInterpreter.setDebugger(null);
   mInterpreter.resume();
  }
 }
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. As before we implement the functionality in the base class.
package com.codeandme.textinterpreter.debugger.model;

public abstract class TextDebugElement extends DebugElement implements ISuspendResume, IDisconnect, ITerminate, IStep {

 // FIXME for full source code see http://codeandme.googlecode.com/svn/trunk/blog/debugger/04/com.codeandme.textinterpreter.debugger/src/com/codeandme/textinterpreter/debugger/model/TextDebugElement.java

 // ************************************************************
 // 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"));
 }
}
There is just one more thing about stepping: the debugger framework distinguishes between a resume request and step requests. Therefore we added a new debugger state: STEPPING. The TextDebugger class needs to keep track of the resume type (continue, step over, step into, step return) and reports corresponding events back to the DebugTarget:
package com.codeandme.textinterpreter.debugger;

public class TextDebugger implements IDebugger, IEventProcessor {

 // FIXME for full source code see http://codeandme.googlecode.com/svn/trunk/blog/debugger/04/com.codeandme.textinterpreter.debugger/src/com/codeandme/textinterpreter/debugger/TextDebugger.java

 private boolean mIsStepping = false;

 @Override
 public void resumed() {
  fireEvent(new ResumedEvent(mIsStepping ? ResumedEvent.STEPPING : ResumedEvent.CONTINUE));
 }

 @Override
 public boolean isBreakpoint(final int lineNumber) {
  return mIsStepping;
 }

 @Override
 public void handleEvent(final IDebugEvent event) {
  System.out.println("Debugger.handleEvent() " + event);

  if (event instanceof ResumeRequest) {
   mIsStepping = (((ResumeRequest) event).getType() == ResumeRequest.STEP_OVER);
   mInterpreter.resume();
  }
 }
}
The TextDebugTarget finally needs to fire an adequate event for the debug framework:
package com.codeandme.textinterpreter.debugger.model;

public class TextDebugTarget extends TextDebugElement implements IDebugTarget, IEventProcessor {

 // FIXME for full source code see http://codeandme.googlecode.com/svn/trunk/blog/debugger/04/com.codeandme.textinterpreter.debugger/src/com/codeandme/textinterpreter/debugger/model/TextDebugTarget.java

 @Override
 public void handleEvent(final IDebugEvent event) {

  if (!isDisconnected()) {
   System.out.println("Target.handleEvent() " + event);

   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