Monday, November 18, 2013

Debugger 6: Debugger breakpoint integration

After we have breakpoints available in the UI we need to teach our debug model and debugger to use them.

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: Adding breakpoint support to the model

Debuggers generally should suspend when a new source file is loaded. Then the model can attach breakpoints to the debugger and resume operation afterwards.

Additionally we need to listen for breakpoint changes to add new breakpoints to the debugger or to remove them when they get disabled or removed by the user.

Lets start with the listener implementation:
public class TextDebugTarget extends TextDebugElement implements IDebugTarget, IEventProcessor {

 @Override
 public boolean supportsBreakpoint(final IBreakpoint breakpoint) {
  if (fFile.equals(breakpoint.getMarker().getResource())) {
   // breakpoint on our source file
   return true;
  }

  return false;
 }

 private boolean isEnabledBreakpoint(IBreakpoint breakpoint) {
  try {
   return breakpoint.isEnabled() && DebugPlugin.getDefault().getBreakpointManager().isEnabled();
  } catch (CoreException e) {
   // ignore invalid breakpoint
  }

  return false;
 }

 // ************************************************************
 // IBreakpointListener
 // ************************************************************

 @Override
 public void breakpointAdded(final IBreakpoint breakpoint) {
  if ((supportsBreakpoint(breakpoint)) && (isEnabledBreakpoint(breakpoint)))
   fireModelEvent(new BreakpointRequest(breakpoint, BreakpointRequest.ADDED));
 }

 @Override
 public void breakpointRemoved(final IBreakpoint breakpoint, final IMarkerDelta delta) {
  if (supportsBreakpoint(breakpoint))
   fireModelEvent(new BreakpointRequest(breakpoint, BreakpointRequest.REMOVED));
 }

 @Override
 public void breakpointChanged(final IBreakpoint breakpoint, final IMarkerDelta delta) {
  breakpointRemoved(breakpoint, delta);
  breakpointAdded(breakpoint);
 }
}
As breakpoints might be targeting non relevant source files or even other target languages we need to verify that a breakpoint is related to our source file. If yes, we send an adequate event to our debugger.

While there are no further checks for a breakpoint removal, we only add active breakpoints. There exist two separate enablements we need to verify: the breakpoint itself can be disabled and/or the BreakpointManager can be disabled globally.

Next we need to take care that we track breakpoint changes accordingly by subscribing to the BreakpointManager.
public class TextDebugTarget extends TextDebugElement implements IDebugTarget, IEventProcessor {

 @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
    setState(State.SUSPENDED);

    // add breakpoint listener
    DebugPlugin.getDefault().getBreakpointManager().addBreakpointListener(this);

    // attach deferred breakpoints to debugger
    IBreakpoint[] breakpoints = DebugPlugin.getDefault().getBreakpointManager().getBreakpoints(getModelIdentifier());
    for (IBreakpoint breakpoint : breakpoints)
     breakpointAdded(breakpoint);

    // inform eclipse of suspended state
    fireSuspendEvent(DebugEvent.CLIENT_REQUEST);

   } else if (event instanceof TerminatedEvent) {
    // debugger is terminated
    setState(State.TERMINATED);

     // unregister breakpoint listener
     DebugPlugin.getDefault().getBreakpointManager().removeBreakpointListener(this);

    // we do not need our dispatcher anymore
    fDispatcher.terminate();

    // inform eclipse of terminated state
    fireTerminateEvent();
   }
  }
 }
}
On a DebuggerStartedEvent we need to set all relevant breakpoints. If we stay suspended or immediately call resume() is a matter of taste.

Step 2: Adding breakpoint support to the debugger

The debugger needs to cache breakpoints and check them on a line change event. In our case we will simply track line numbers where we want to suspend:
public class TextDebugger implements IDebugger, IEventProcessor {

 private final Collection<Integer> fBreakpoints = new HashSet<>();

 @Override
 public boolean isBreakpoint(final int lineNumber) {
  if (fBreakpoints.contains(lineNumber))
   return true;

  return fIsStepping;
 }

 @Override
 public void handleEvent(final IDebugEvent event) {

  [...]

  } else if (event instanceof BreakpointRequest) {
   int line = ((BreakpointRequest) event).getBreakpoint().getMarker().getAttribute(IMarker.LINE_NUMBER, -1);
   if (line != -1) {
    if (((BreakpointRequest) event).getType() == BreakpointRequest.ADDED)
     fBreakpoints.add(line);

    else if (((BreakpointRequest) event).getType() == BreakpointRequest.REMOVED)
     fBreakpoints.remove(line);
   }
  }
 }
}

Breakpoints are stored for a dedicated IDebugModelPresentation. We introduced one in the previous tutorial. Now we need to let TextDebugElement.getModelIdentifier() refer to this ID. If you do not set this identifier, you will not be able to find any breakpoints.

The mechanisms for setting and caching breakpoints might get a bit more complicated for a real world solution as you might support different kinds of breakpoints like function breakpoints or watchpoints.

No comments:

Post a Comment