How to write a DSF-based debugger

Summary

This tutorial describes the process of integrating a debugger with Eclipse using the Debugger Services Framework (DSF) developed by the DSDP Device Debugging project.

Table of Contents

Copyright

Copyright (c) 2005, 2008 IBM Corporation and others. All rights reserved. This program and the accompanying materials are made available under the terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at http://www.eclipse.org/legal/epl-v10.html

Contributors:

1 Push Down Automata (PDA)

The Push Down Automata (PDA) debugger example is used as the basis for this tutorial.  Before starting the tutorial it is best to familiarize with the features of the debugger.

1.1 Running the Example

Launch the PDA debugger with these twelve "easy" steps:
  1. Download and install Eclipse development environment, either the Eclipse Classic 3.4 or Eclipse IDE for C/C++ Developers
  2. Install the DSF SDK feature to build against, by performing either:
    1. Using update manager, install the Debugger Services Framework end-user and extender SDK, found in the Ganymede Discovery Site under Remote Access and Device Development.
    2. Check out org.eclipse.dd.dsf and org.eclipse.dd.dsf.ui plugins, found in the /cvsroot/dsdp repository under org.eclipse.dd.dsf/plugins directory.
  3. Check out the org.eclipse.dd.examples.pda and org.eclipse.dd.examples.pda.ui plugins, found /cvsroot/dsdp in the org.eclipse.dd.dsf/plugins directory.
  4. Build the PDA plugins.
  5. Create an instance of an Eclipse Application launch configuration and launch it in Debug mode.
  6. Switch to the new Eclipse IDE Application
  7. Create a new empty project:
    1. Select the File->New->Project... action
    2. Select General->Project in the New Project dialog.
    3. Enter a name for the new project (e.g. "PDA")
  8. Link in the folder with PDA examples from the org.eclipse.dd.examples.pda plugin.
    1. Right-click on the new Project and select New->Folder
    2. Click on the Advanced >> button at the bottom of the New Folder dialog.
    3. Select the Link to folder in the filesystem check box.
    4. Type in or browse to the samples directory found in the org.eclipse.dd.examples.pda plugin.
  9. Open the PDA editor by double-clicking on the PDA file (e.g. fibonacci.pda).  See note below.
  10. Set a breakpoint in the program by double-clicking in the editor gutter.
  11. Launch the PDA debugger
    1. Set the dsfPerlDebugger variable to point to the Perl executable in your system.  Variables can be set in the Windows->Preferences->Run/Debug->String Substitution preference page.
    2. Create a new launch configuration of type DSF PDA Application
    3. In the Main tab, enter the workspace path to the program name (e.g. /PDA/samples/fibonacci.pda).
    4. Select Debug to launch the PDA debugger.
  12. Step, select stack frames, debug...
Note: If the Debug Platform example plugins (org.eclipse.debug.examples.*) were previously installed in the same workspace as the DSF PDA example, the two examples will both have an editor registered for the ".pda" file type.  To ensure that the right editor is opened, right click on the PDA file in the Navigator, and select Open With->PDA(DSF) Editor.  The editor that is opened should have the DSF-PDA icon:

Note: A Perl interpreter is required for PDA. Linux®™ comes with Perl. For Microsoft® Windows®, we use either ActivePerl (http://www.activeperl.com/) or Indigo Perl (http://www.indigostar.com/).

1.2 Language

To demonstrate how to write a debugger for Eclipse, we need a language and a run time to debug. For this example, we chose an enhanced push down automata (PDA) assembly language and a simple interpreter implemented in Perl. Each line contains a single operation and any number of arguments. Our language differs from a standard PDA in two major ways:

Here is an annotated example of the Fibonacci computation (note that the annotations themselves are not valid syntax in this language – in this language, all comments must start at column 1 and be the entire line):


samples/fibonacci.pda

push 6
call Fibonacci function call with one argument on the data stack
output print result to stdout
halt
#
# f(n) = f(n-1) + f(n-2)
# f(0) = 1
# f(1) = 1
#
:fibonacci
var n define variable n on control stack
pop $n get n from data stack
push $n
branch_not_zero gt0
push 1 f(0) = 1

return return with one value on data stack
:gt0
push $n
dec
branch_not_zero gt1
push 1 f(1) = 1
return return with one value on data stack
:gt1
push $n stack: n
dec stack: n-1
call fibonacci stack: f(n-1)

push $n stack: f(n-1) n
dec stack: f(n-1) n-1
dec stack: f(n-1) n-2
call Fibonacci stack: f(n-1) f(n-2)
add stack: f(n-1)+f(n-2)
return return with one value on data stack

1.3 Debug Protocol

Our PDA assembly language interpreter can be started in either run mode or debug mode. When started in debug mode, the interpreter listens for debug commands on a specified local TCP/IP socket and sends debug events to a separate local TCP/IP socket.  A detailed description of the protocol can be found in org.eclipse.dd.examples.pda/pdavm/docs/protocol.html, but the lists below show a quick overview.

The commands include:

The debug events that are reported asynchronously to the second socket include:

2 Step 1 - Launching

The first task in integrating a debugger in Eclipse is creating and managing the debugger process.  The Eclipse Platform provides an extensive API for this purpose, which is nicely presented in the We Have Lift-off: The Launching Framework in Eclipse article.  This section (as this tutorial) concentrates on the DSF-specific tasks of launching the PDA debugger.

2.1 Launch Delegate

At first glance, there's nothing unusual about the PDA debugger launch delegate.  Just like the Debug Platform version it:
  1. finds the Perl executable,
  2. finds free socket ports for debugger communication,
  3. finds the PDA program
  4. launches Perl to run the interpreter
The major difference is that it does not create an instance of the standard debug model IDebugTerget object.  Instead it implements the getLaunch() method in the ILaunchConfigurationDelegate2 extension interface, in order to create a custom launch object:

org.eclipse.dd.examples.pda.launch.PDALaunchDelegate - getLaunch()

 51:  public ILaunch getLaunch(ILaunchConfiguration configuration, String mode) throws CoreException {

52: // Need to configure the source locator before creating the launch
53: // because once the launch is created and added to launch manager,
54: // the adapters will be created for the whole session, including
55: // the source lookup adapter.
56: ISourceLocator locator = getSourceLocator(configuration);

58: return new PDALaunch(configuration, mode, locator);
59: }

2.2 PDALaunch

The PDALaunch object plays two main roles:
  1. Serve as the root element of the PDA View Model hierarchy
  2. Manage the lifecycle of the DSF session, its services, and the executor that belongs to the session. 
The first task will be described in the View Model section, the second task is described here. 

Even though the PDALaunch constructor is called long before the debugging services are created, the session and the executor need to be available to the UI clients that present the launch object in the Debug view.

org.eclipse.dd.examples.pda.launch.PDALaunch - <<constructor>>

 65:     public PDALaunch(ILaunchConfiguration launchConfiguration, String mode, ISourceLocator locator) {
66: super(launchConfiguration, mode, locator);

68: // Create the dispatch queue to be used by debugger control and services
69: // that belong to this launch
70: final DefaultDsfExecutor dsfExecutor = new DefaultDsfExecutor(PDAPlugin.ID_PDA_DEBUG_MODEL);
71: dsfExecutor.prestartCoreThread();
72: fExecutor = dsfExecutor;
73: fSession = DsfSession.startSession(fExecutor, PDAPlugin.ID_PDA_DEBUG_MODEL);
74: }
As the last step of the launch process, after the Perl process is started, the launch delegate calls the launch to initialize the DSF services.  There is an expected race condition between initializeServices() and shutdownServices() routines in that the PDA process may run to completion and exit while the initialize services routine is still running.  Also, the user may terminate the program while the initialization sequene is still running.  The use of fInitializationSequence variable and other flags protects deals with this race condition.

org.eclipse.dd.examples.pda.launch.PDALaunch - intializeServices()

 90:     @ConfinedToDsfExecutor("getSession().getExecutor()")
91: public void initializeServices(String program, int requestPort, int eventPort, final RequestMonitor rm)
92: {
93: // Double-check that we're being called in the correct thread.
94: assert fExecutor.isInExecutorThread();

96: // Check if shutdownServices() was called already, which would be
97: // highly unusual, but if so we don't need to do anything except set
98: // the initialized flag.
99: synchronized(this) {
100: if (fShutDown) {
101: fInitialized = true;
102: return;
103: }
104: }

106: // Register the launch as listener for services events.
107: fSession.addServiceEventListener(PDALaunch.this, null);

109: // The initialization sequence is stored in a field to allow it to be
110: // canceled if shutdownServices() is called before the sequence
111: // completes.
112: fInitializationSequence = new PDAServicesInitSequence(
113: getSession(), program, requestPort, eventPort,
114: new RequestMonitor(ImmediateExecutor.getInstance(), rm) {
115: @Override
116: protected void handleCompleted() {
117: // Set the initialized flag and check whether the
118: // shutdown flag is set. Access the flags in a
119: // synchronized section as these flags can be accessed
120: // on any thread.
121: boolean doShutdown = false;
122: synchronized (this) {
123: fInitialized = true;
124: fInitializationSequence = null;
125: if (fShutDown) {
126: doShutdown = true;
127: }
128: }

130: if (doShutdown) {
131: // If shutdownServices() was already called, start the
132: // shutdown sequence now.
133: doShutdown(rm);
134: } else {
135: // If there was an error in the startup sequence,
136: // report the error to the client.
137: if (getStatus().getSeverity() == IStatus.ERROR) {
138: rm.setStatus(getStatus());
139: }
140: rm.done();
141: }
142: fireChanged();
143: }
144: });

146: // Finally, execute the sequence.
147: getSession().getExecutor().execute(fInitializationSequence);
148: }
Due to race conditions between debugger events and user commands, the shutdownServices() routine may be invoked more than once.  The shutdown logic must protect against these race conditions.

org.eclipse.dd.examples.pda.launch.PDALaunch - shutdownServices()

202:     @ConfinedToDsfExecutor("getSession().getExecutor()")
203: public void shutdownServices(final RequestMonitor rm) {
204: // Check initialize and shutdown flags to determine if the shutdown
205: // sequence can be called yet.
206: boolean doShutdown = false;
207: synchronized (this) {
208: if (!fInitialized && fInitializationSequence != null) {
209: // Launch has not yet initialized, try to cancel the
210: // shutdown sequence.
211: fInitializationSequence.cancel(false);
212: } else {
213: doShutdown = !fShutDown && fInitialized;
214: }
215: fShutDown = true;
216: }

218: if (doShutdown) {
219: doShutdown(rm);
220: } else {
221: rm.done();
222: }
223: }

225: @ConfinedToDsfExecutor("getSession().getExecutor()")
226: private void doShutdown(final RequestMonitor rm) {
227: fExecutor.execute( new PDAServicesShutdownSequence(
228: fExecutor, fSession.getId(),
229: new RequestMonitor(fSession.getExecutor(), rm) {
230: @Override
231: public void handleCompleted() {
232: fSession.removeServiceEventListener(PDALaunch.this);
233: if (!getStatus().isOK()) {
234: PDAPlugin.getDefault().getLog().log(new MultiStatus(
235: PDAPlugin.PLUGIN_ID, -1, new IStatus[]{getStatus()}, "Session shutdown failed", null)); //$NON-NLS-1$
236: }
237: // Last order of business, shutdown the dispatch queue.
238: DsfSession.endSession(fSession);
239: // endSession takes a full dispatch to distribute the
240: // session-ended event, finish step only after the dispatch.
241: fExecutor.shutdown();
242: fireTerminate();

244: rm.setStatus(getStatus());
245: rm.done();
246: }
247: }) );
248: }

2.3 Launch/Shutdown Sequence

The actual task of calling the asynchronous IDsfService's initialize() and shutdown() methods is implemented using the Sequence object.  The following listing shows part of the declaration of the Sequence.Step objects which perform the service initialization:

org.eclipse.dd.examples.pda.launch.PDAServicesInitSequence - fSteps

 38:     Step[] fSteps = new Step[] {
39: new Step()
40: {
41: @Override
42: public void execute(RequestMonitor requestMonitor) {
43: // Create the connection to PDA debugger.
44: fCommandControl = new PDACommandControl(fSession, fProgram, fRequestPort, fEventPort);
45: fCommandControl.initialize(requestMonitor);
46: }
47: },
48: new Step() {
49: @Override
50: public void execute(RequestMonitor requestMonitor) {
51: // Start the run control service.
52: fRunControl = new PDARunControl(fSession);
53: fRunControl.initialize(requestMonitor);
54: }
55: },
56: new Step() {
57: @Override
58: public void execute(RequestMonitor requestMonitor) {
59: // Start the service to manage step actions.
60: new StepQueueManager(fSession).initialize(requestMonitor);
61: }
62: },
...

3 Step 2 - Connecting 

With the launch framework in place, the debugger back end is running and the DSF session and executor are started.  The next step is to create the first service and to connect to the debugger.  DSF defines a debug interface: ICommandControl which abstracts a debugger connection as a facility that processes commands and generates events.  The ICommandControl method allow for three major functions:
  1. Queue/Remove/Cancel commands - It is assumed that the command control service uses a queue to hold commands that are to be sent to the debugger.  As long as commands are still in the queue, clients can remove the commands so they are never sent.  Even after the commands are sent, the clients may request to cancel a running command, although there is no guarantee that the debugger supports that.
  2. Listening to Commands Queued/Sent/Completed - Clients can listen to all command traffic in order to implement custom processing.
  3. Listening to Events - Events are messages from the debugger which are not direct responses to any commands.  Many clients need to listen to events such as target state change events.

Image 1: PDA Command Control Diagram

3.1 Synchronization

Since there are several threads being used by the PDA Command Control protecting state data becomes very important.

Most of the state data in the command control service is protected using the session thread, i.e. they can only be accessed while executing in the session executor's thread:

org.eclipse.dd.examples.pda.service.PDACommandControl - members declaration

 56:     // Parameters that the command control is created with.
57: final private String fProgram;
58: final private int fRequestPort;
59: final private int fEventPort;

61: // Queue of commands waiting to be sent to the debugger. As long as commands
62: // are in this queue, they can still be removed by clients.
63: private final List<CommandHandle> fCommandQueue = new LinkedList<CommandHandle>();
64:
65: // Queue of commands that are being sent to the debugger. This queue is read
66: // by the send job, so as soon as commands are inserted into this queue, they can
67: // be considered as sent.
68: @ThreadSafe
69: private final BlockingQueue<CommandHandle> fTxCommands = new LinkedBlockingQueue<CommandHandle>();
70:
71: // Flag indicating that the PDA debugger started
72: private boolean fStarted = false;
73:
74: // Flag indicating that the PDA debugger has been disconnected
75: @ThreadSafe
76: private boolean fTerminated = false;
77:
78: // Data Model context of this command control.
79: private PDAProgramDMContext fDMContext;

81: // Synchronous listeners for commands and events.
82: private final List<ICommandListener> fCommandListeners = new ArrayList<ICommandListener>();
83: private final List<IEventListener> fEventListeners = new ArrayList<IEventListener>();
84:
85: // Sockets for communicating with PDA debugger
86: @ThreadSafe
87: private Socket fRequestSocket;
88: @ThreadSafe
89: private PrintWriter fRequestWriter;
90: @ThreadSafe
91: private BufferedReader fRequestReader;
92: @ThreadSafe
93: private Socket fEventSocket;
94: @ThreadSafe
95: private BufferedReader fEventReader;

97: // Jobs servicing the sockets.
98: private EventDispatchJob fEventDispatchJob;
99: private CommandSendJob fCommandSendJob;
Following is an example of how the access to session-thread protected variables is implemented.

org.eclipse.dd.examples.pda.service.PDACommandControl.EventDispatchJob - run()

300:         protected IStatus run(IProgressMonitor monitor) {
301: while (!isTerminated()) {
302: try {
303: // Wait for an event.
304: final String event = fEventReader.readLine();
305: if (event != null) {
306: try {
307: // Process the event in executor thread.
308: getExecutor().execute(new DsfRunnable() {
309: public void run() {
310: processEventReceived(event);
311: }
312: });
313: } catch (RejectedExecutionException e) {}
314: } else {
315: break;
316: }
317: } catch (IOException e) {
318: break;
319: }
320: }
321: if (!isTerminated()) {
322: // Exception from the event socket is an indicator that the PDA debugger
323: // has exited. Call setTerminated() in executor thread.
324: try {
325: getExecutor().execute(new DsfRunnable() {
326: public void run() {
327: setTerminated();
328: }
329: });
330: } catch (RejectedExecutionException e) {}
331: }
332: return Status.OK_STATUS;
333: }
334:
335: }

3.2 Command/Event Listeners

As mentioned before there are two types of listeners that can be registered with the commands control: event listeners and command listeners.  The most important feature of these listeners, is that they are called by the command control synchronously.  As a result of this, the command listeners can expect to see the state of the command queue that is consistent with the event they just received.  However, if clients need to modify the queue as a result of the event, they should only do it in a separate runnable, otherwise other command listeners may encounter the command control in an inconsistent state.

org.eclipse.dd.examples.pda.service.PDACommandControl - queueCommand()

337:     public <V extends ICommandResult> void queueCommand(ICommand<V> command, DataRequestMonitor<V> rm) {
338: if (command instanceof AbstractPDACommand<?>) {
339: // Cast from command with "<V extends ICommandResult>" to a more concrete
340: // type to use internally in the command control.
341: @SuppressWarnings("unchecked")
342: AbstractPDACommand<PDACommandResult> pdaCommand = (AbstractPDACommand<PDACommandResult>)command;
343:
344: // Similarly, cast the request monitor to a more concrete type.
345: @SuppressWarnings("unchecked")
346: DataRequestMonitor<PDACommandResult> pdaRM = (DataRequestMonitor<PDACommandResult>)rm;

348: // Add the command to the queue and notify command listeners.
349: fCommandQueue.add( new CommandHandle(pdaCommand, pdaRM) );
350: for (ICommandListener listener : fCommandListeners) {
351: listener.commandQueued(command);
352: }
353:
354: // In a separate dispatch cycle. This allows command listeners to repond to the
355: // command queued event.
356: getExecutor().execute(new DsfRunnable() {
357: public void run() {
358: processQueues();
359: }
360: });
361:
362: } else {
363: PDAPlugin.failRequest(rm, INTERNAL_ERROR, "Unrecognized command: " + command);
364: }
365: }

3.3 PDAProgramDMContext

Finally the command control also declares a Data Model context, which is a parent to all other contexts for a given PDA debugger session.  Each command used with the command control has to implement the ICommand.getContext() method, which returns the context that the command is acting on.  In PDA debugger, this context is always the PDAProgramDMContext instance returned by PDACommandControl.getProgramDMContext(). However in other debuggers this context can have two other functions:
  1. To identify the command control instance - In debugger sessions that connect to multiple back ends, the context can be used to identify which command control should process a given command.
  2. To help control debugger command protocol state - The PDA debug protocol is stateless, which means that any command acts independently of any commands that came before it.  For debuggers which do have protocol state, e.g. GDB/MI, the command control needs to check the context of each command and set the protocol by preceding the command being processed with other commands (such as -thread-select in GDB/MI).

3.4 PDA Commands

To increase type safety and make the code more readable the plain text PDA commands are abstracted using specific command objects.  Below is an example of a command class:

org.eclipse.dd.examples.pda.service.commands.PDADataCommand

 15: /**
16: * Retrieves data stack information
17: *
18: * <pre>
19: * C: data
20: * R: {value 1}|{value 2}|{value 3}|...|
21: * </pre>
22: */
23: @Immutable
24:
public class PDADataCommand extends AbstractPDACommand<PDADataCommandResult> {
25:
26: public PDADataCommand(PDAProgramDMContext context) {
27: super(context, "data");
28: }
29:
30: @Override
31: public PDADataCommandResult createResult(String resultText) {
32: return new PDADataCommandResult(resultText);
33: }
34: }
Here is the corresponding data result class:

org.eclipse.dd.examples.pda.service.commands.PDADataCommandResult

 20: @Immutable
21:
public class PDADataCommandResult extends PDACommandResult {
22:
23: final public String[] fValues;
24:
25: PDADataCommandResult(String response) {
26: super(response);
27: StringTokenizer st = new StringTokenizer(response, "|");
28: List<String> valuesList = new ArrayList<String>();
29:
30: while (st.hasMoreTokens()) {
31: String token = st.nextToken();
32: if (token.length() != 0) {
33: valuesList.add(st.nextToken());
34: }
35: }
36:
37: fValues = new String[valuesList.size()];
38: for (int i = 0; i < valuesList.size(); i++) {
39: fValues[i] = valuesList.get(i);
40: }
41: }
42: }

Note: Command and command results can be stored in a cache as keys and values.  Making them immutable helps guard the integrity of these caches.

4 Step 3 - View Model

4.1 Adapter Glue

The adapter mechanism is something like the glue of Eclipse APIs. It allows objects to be associated together without having any explicit dependencies between each other.  Just like glue it works best when the mating parts are clean and closely fitted together, where just a little glue does the job.  If too much glue is used to put together many odd parts, the whole thing can turn into a big sticky mess that falls apart at the lightest touch.

After connecting to the debugger, the next step is to get something to display in the debugger views.  Flexible Hierarchy viewers depend heavily on the adapter mechanism to associate the presentation classes with the objects being presented.  The first step to connect the DSF View Model for the debugger views is to register an adapter factory for the custom PDALaunch object:

org.eclipse.dd.examples.pda.ui/plugin.xml - PDA adapter factory declaration

   <extension
point="org.eclipse.core.runtime.adapters">
<factory
class="org.eclipse.dd.examples.pda.ui.PDAAdapterFactory"
adaptableType="org.eclipse.dd.examples.pda.launch.PDALaunch">
<adapter type="org.eclipse.debug.internal.ui.viewers.model.provisional.IElementContentProvider"/>
<adapter type="org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxyFactory"/>
</factory>
</extension>

The adapter factory has two jobs:
  1. Return the PDAVMAdapter instance as the content provider for the PDALaunch element.
  2. Register and manage the life-cycle of all other adapters required for a functioning debugger.
The first job is performed by the IAdapterFactory.getAdapter() method listed below:

org.eclipse.dd.examples.pda.ui.PDAAdapterFactory - getAdapter()

157:     public Object getAdapter(Object adaptableObject, Class adapterType) {
158: if (!(adaptableObject instanceof PDALaunch)) return null;

160: PDALaunch launch = (PDALaunch)adaptableObject;

162: // Find the correct set of adapters based on the launch. If not found
163: // it means that we have a new launch, and we have to create a
164: // new set of adapters.
165: LaunchAdapterSet adapterSet;
166: synchronized(fLaunchAdapterSets) {
167: adapterSet = fLaunchAdapterSets.get(launch);
168: if (adapterSet == null) {
169: adapterSet = new LaunchAdapterSet(launch);
170: fLaunchAdapterSets.put(launch, adapterSet);
171: }
172: }
173:
174: // Returns the adapter type for the launch object.
175: if (adapterType.equals(IElementContentProvider.class)) return adapterSet.fViewModelAdapter;
176: else if (adapterType.equals(IModelProxyFactory.class)) return adapterSet.fViewModelAdapter;
177: else return null;
178: }
The LaunchAdapterSet constructor is responsible for creating and registering all other adapters for the new debug session.  However, instead of using the platform adapter factory mechanism, the adapters are registered with the DSF session object and are returned as adapters to the Data Model's IDMContext object.

org.eclipse.dd.examples.pda.ui.PDAAdapterFactory.LaunchAdapterSet - <<constructor>>

 85:         LaunchAdapterSet(PDALaunch launch) {
86: // Initialize launch and session.
87: fLaunch = launch;
88: DsfSession session = launch.getSession();
89:
90: // Initialize VM
91: fViewModelAdapter = new PDAVMAdapter(session);

93: // Initialize source lookup
94: fSourceDisplayAdapter = new MISourceDisplayAdapter(session, (ISourceLookupDirector)launch.getSourceLocator());
95: session.registerModelAdapter(ISourceDisplay.class, fSourceDisplayAdapter);
96:
97: // Initialize retargetable command handler.
98: fStepIntoCommand = new DsfStepIntoCommand(session);
99: fStepOverCommand = new DsfStepOverCommand(session);
100: fStepReturnCommand = new DsfStepReturnCommand(session);
101: fSuspendCommand = new DsfSuspendCommand(session);
102: fResumeCommand = new DsfResumeCommand(session);
103: fTerminateCommand = new PDATerminateCommand(session);
104: session.registerModelAdapter(IStepIntoHandler.class, fStepIntoCommand);
105: session.registerModelAdapter(IStepOverHandler.class, fStepOverCommand);
106: session.registerModelAdapter(IStepReturnHandler.class, fStepReturnCommand);
107: session.registerModelAdapter(ISuspendHandler.class, fSuspendCommand);
108: session.registerModelAdapter(IResumeHandler.class, fResumeCommand);
109: session.registerModelAdapter(ITerminateHandler.class, fTerminateCommand);

111: // Initialize debug model provider
112: fDebugModelProvider = new IDebugModelProvider() {
113: public String[] getModelIdentifiers() {
114: return new String[] { PDAPlugin.ID_PDA_DEBUG_MODEL };
115: }
116: };
117: session.registerModelAdapter(IDebugModelProvider.class, fDebugModelProvider);
118:
119: // Register the launch as an adapter This ensures that the launch,
120: // and debug model ID will be associated with all DMContexts from this
121: // session.
122: session.registerModelAdapter(ILaunch.class, fLaunch);
123: }
Note: Most of the common debug actions, such as the resume/suspend/step commands in Debug view, have been converted into re-targetable commands.  This means that the given action uses the adapter mechanism to retrieve the command handler from the selected element.  For a DSF-based debugger integration these command handlers must be explicitly created and registered with the session so that they are returned by the Data Model IDMContext.getAdapter() implementation.

Each adapter set is created new for each PDA debug session.  They also need to be disposed when they are no longer needed.  It could be expected that the adapters should be disposed when the PDA debugger is terminated.  However, the terminated debug session still appears in the Debug view, and this requires the adapters to be present.  Instead, the adapters are removed when the corresponding PDA launch is removed:

org.eclipse.dd.examples.pda.ui.PDAAdapterFactory - launchesRemoved()

185:     public void launchesRemoved(ILaunch[] launches) {
186: // Dispose the set of adapters for a launch only after the launch is
187: // removed from the view. If the launch is terminated, the adapters
188: // are still needed to populate the contents of the view.
189: for (ILaunch launch : launches) {
190: if (launch instanceof PDALaunch) {
191: PDALaunch pdaLaunch = (PDALaunch)launch;
192: synchronized(fLaunchAdapterSets) {
193: if ( fLaunchAdapterSets.containsKey(pdaLaunch) ) {
194: fLaunchAdapterSets.remove(pdaLaunch).dispose();
195: }
196: }
197: }
198: }
199: }

4.2 PDA View Model

The PDAVMAdapter creates the VM Providers on demand for each debugger view it supports:

org.eclipse.dd.examples.pda.ui.viewmodel.PDAVMAdapter - createViewModelProvider()

 43:     protected AbstractDMVMProvider createViewModelProvider(IPresentationContext context) {
44: if ( IDebugUIConstants.ID_DEBUG_VIEW.equals(context.getId()) ) {
45: return new PDALaunchVMProvider(this, context, getSession());
46: } else if (IDebugUIConstants.ID_VARIABLE_VIEW.equals(context.getId()) ) {
47: return new VariableVMProvider(this, context, getSession());
48: } else if (IDebugUIConstants.ID_EXPRESSION_VIEW.equals(context.getId()) ) {
49: return new ExpressionVMProvider(this, context, getSession());
50: }
51: return null;
52: }
Note: The VM Adapter is responsible for creating providers for all debugger views supported by the given debugger.  In case of PDA, the registers and modules views are not supported, therefore the corresponding VM Providers are not created.

Note: The Memory view is not a Flexible Hierarchy view and so is not supported by the View Model.  For an example of integrating a memory view, see the DSF-based GDB debugger integration.

4.3 Launch VM Provider

"Launch" actually refers to the internal name of the Debug view.  The PDA debugger has a somewhat simpler presentation in Debug view than most debuggers because it does not support multiple threads so it has the thread node shown directly below the launch node.


Image 2: Debug view screen capture
The PDALaunchVMProvider constructor creates the VM Nodes and arranges them in a hierarchy that mirrors the screen-shot in Image 2.

org.eclipse.dd.examples.pda.ui.viewmodel.launch.PDALaunchVMProvider - <<constructor>>

 50:     public PDALaunchVMProvider(AbstractVMAdapter adapter, IPresentationContext presentationContext, DsfSession session) 
51: {
52: super(adapter, presentationContext, session);
53:
54: IRootVMNode launchNode = new LaunchRootVMNode(this);
55: setRootNode(launchNode);

57: // Launch node is a parent to the processes and program nodes.
58: IVMNode pdaProgramNode = new PDAProgramVMNode(this, getSession());
59: IVMNode processesNode = new StandardProcessVMNode(this);
60: addChildNodes(launchNode, new IVMNode[] { pdaProgramNode, processesNode});
61:
62: // Stack frames node is under the PDA program node.
63: IVMNode stackFramesNode = new StackFramesVMNode(this, getSession());
64: addChildNodes(pdaProgramNode, new IVMNode[] { stackFramesNode });

66: // Register the LaunchVM provider as a listener to debug and launch
67: // events. These events are used by the launch and processes nodes.
68: DebugPlugin.getDefault().addDebugEventListener(this);
69: DebugPlugin.getDefault().getLaunchManager().addLaunchListener(this);
70: }

4.4 PDA Program VM Node

Elements

The PDA Program Node is the most complex component of the PDA View Model.  It supplies an element representing the PDA program and it operates in three modes:
  1. Not Initialized - After the PDALaunch object has been created, but the services have not yet been initialized.  In this mode the PDA Program is not shown in Debug view.
  2. Running - After the PDALaunch and PDACommandControl are initialized, the PDAProgramDMContext object is used as the underlying Data Model element shown in Debug view. 
  3. Shut down - After a program has terminated, it is still shown in the Debug view, as is consistent with the established workflow.  However, at this point the PDACommandControl service is shut down and the PDAProgramDMContext is not available.  Hence, a separate TerminatedProgramVMContext wrapper element is used which does not have an underlying Data Model element.
The following two methods implement the logic of supplying the elements for the above modes:

org.eclipse.dd.examples.pda.ui.viewmodel.launch.PDALaunchVMNode - update(IChildrenUpdate[])

119:     @Override
120: public void update(IChildrenUpdate[] updates) {
121: for (IChildrenUpdate update : updates) {
122: PDALaunch launch = findLaunchInPath(update.getElementPath());
123: if (launch != null && launch.isInitialized() && launch.isShutDown()) {
124: // If the debug session has been shut down, add a dummy
125: // VM context representing the PDA thread.
126: update.setChild(new TerminatedProgramVMContext(getVMProvider().getVMAdapter(), this), 0);
127: update.done();
128: } else {
129: super.update(new IChildrenUpdate[] { update });
130: }
131: }
132: }
133:
134: @Override
135: protected void updateElementsInSessionThread(final IChildrenUpdate update) {
136: // Get the instance of the service. Note that there is no race condition
137: // in getting the service since this method is called only in the
138: // service executor thread.
139: final PDACommandControl commandControl = getServicesTracker().getService(PDACommandControl.class);

141: // Check if the service is available. If it is not, no elements are
142: // updated.
143: if (commandControl == null) {
144: handleFailedUpdate(update);
145: return;
146: }
147:
148: update.setChild(createVMContext(commandControl.getProgramDMContext()), 0);
149: update.done();
150: }

Label

Calculating the label for the PDA program element is also split into two parts depending on whether the program is terminated.  Similarly as when calculating the element, if the program is terminated, the label is calculated in the View Model thread, if the program is running, the execution is switched to the session executor thread.  This is accomplished in the update(ILabelUpdate[]) implementation:

org.eclipse.dd.examples.pda.ui.viewmodel.launch.PDALaunchVMNode - update(ILabelUpdate[])

152:     public void update(final ILabelUpdate[] updates) {
153: for (final ILabelUpdate update : updates) {
154: if (update.getElement() instanceof TerminatedProgramVMContext) {
155: // If the element is a terminated program, update the label
156: // in the View Model thread.
157: updateTerminatedThreadLabel(update);
158: } else {
159: // If the element is the PDA Program context, try to switch
160: // to the DSF session thread before updating the label.
161: try {
162: getSession().getExecutor().execute(new DsfRunnable() {
163: public void run() {
164: updateProgramLabelInSessionThread(update);
165: }});
166: } catch (RejectedExecutionException e) {
167: // Acceptable race condition: DSF session terminated.
168: handleFailedUpdate(update);
169: }
170: }
171: }
172: }
The updateProgramLabelInSessionThread() is rather long, but it is useful to look at it in detail because it is representative of what all label providers must do.

org.eclipse.dd.examples.pda.ui.viewmodel.launch.PDALaunchVMNode - updateProgramLabelInSessionThread()

174:     @ConfinedToDsfExecutor("getSession().getExecutor()")
175: private void updateProgramLabelInSessionThread(final ILabelUpdate update) {
176: // Get a reference to the run control service.
177: final IRunControl runControl = getServicesTracker().getService(IRunControl.class);
178: if (runControl == null) {
179: handleFailedUpdate(update);
180: return;
181: }
182:
183: // Find the PDA program context.
184: final PDAProgramDMContext programCtx =
185: findDmcInPath(update.getViewerInput(), update.getElementPath(), PDAProgramDMContext.class);

187: // Call service to get current program state
188: final boolean isSuspended = runControl.isSuspended(programCtx);

190: // Set the program icon based on the running state of the program.
191: String imageKey = null;
192: if (isSuspended) {
193: imageKey = IDebugUIConstants.IMG_OBJS_THREAD_SUSPENDED;
194: } else {
195: imageKey = IDebugUIConstants.IMG_OBJS_THREAD_RUNNING;
196: }
197: update.setImageDescriptor(DebugUITools.getImageDescriptor(imageKey), 0);

199: // Retrieve the last state change reason
200: runControl.getExecutionData(
201: programCtx,
202: new DataRequestMonitor<IExecutionDMData>(ImmediateExecutor.getInstance(), null)
203: {
204: @Override
205: public void handleCompleted(){
206: // If the request failed, fail the udpate.
207: if (!getStatus().isOK()) {
208: handleFailedUpdate(update);
209: return;
210: }
211:
212: // Compose the thread name string.
213: final StringBuilder builder = new StringBuilder();
214:
215: builder.append("PDA [");
216: builder.append(programCtx.getProgram());
217: builder.append("]");
218:
219: if(isSuspended) {
220: builder.append(" (Suspended");
221: } else {
222: builder.append(" (Running");
223: }
224: // Reason will be null before ContainerSuspendEvent is fired
225: if(getData().getStateChangeReason() != null) {
226: builder.append(" : ");
227: builder.append(getData().getStateChangeReason());
228: }
229: builder.append(")");
230: update.setLabel(builder.toString(), 0);
231: update.done();
232: }
233: });
234: }

Delta

Translating the Data Model events into IModelDelta objects that can be processed by the Flexible Hierarchy views, is the most complicated task performed by the View Model infrastructure.  The model deltas require that a path be formed by the IModelDelta objects which matches the hierarchy elements found in the view, including such details as indexes of elements and their number at each level.  Normally this requires that the model event handler should know the full hierarchy of the elements in a given view.  However, with the DSF View Model, this logic is split into two parts:
  1. The event handler in the VM Provider
  2. The build delta methods in the VM Nodes.
The debug model event handler below is an example of the event handler implementation in the VM Provider.  The event handler for Data Model events is already implemented in the AbstractDMVMProvider base class:

org.eclipse.dd.examples.pda.ui.viewmodel.launch.PDALaunchVMProvider - handleDebugEvents()

 73:     public void handleDebugEvents(final DebugEvent[] events) {
74: if (isDisposed()) return;
75:
76: // This method may be called on any thread. Switch to the
77: // view model executor thread before processing.
78: try {
79: getExecutor().execute(new Runnable() {
80: public void run() {
81: if (isDisposed()) return;
82:
83: for (final DebugEvent event : events) {
84: handleEvent(event);
85: }
86: }});
87: } catch (RejectedExecutionException e) {
88: // Ignore. This exception could be thrown if the view model is being
89: // shut down.
90: }
91: }
The PDA program node implements methods to add the delta nodes for its elements:

org.eclipse.dd.examples.pda.ui.viewmodel.launch.PDAProgramVMNode - buildDelta()

265:     public int getDeltaFlags(Object e) {
266: if(e instanceof IResumedDMEvent || e instanceof ISuspendedDMEvent) {
267: return IModelDelta.STATE;
268: }
269: if (e instanceof PDAStartedEvent) {
270: return IModelDelta.EXPAND | IModelDelta.SELECT;
271: }
272: return IModelDelta.NO_CHANGE;
273: }
274:
275: public void buildDelta(Object e, VMDelta parentDelta, int nodeOffset, RequestMonitor rm) {
276: if(e instanceof IResumedDMEvent || e instanceof ISuspendedDMEvent) {
277: // If a suspended/resumed event is received, just update the
278: // state of the program. StackFramesVMNode will take care of
279: // refreshing the stack frames.
280: parentDelta.addNode(createVMContext(((IDMEvent<?>)e).getDMContext()), IModelDelta.STATE);
281: }
282: if (e instanceof PDAStartedEvent) {
283: // When debug session is started expand and select the program.
284: // If the program hits a breakpoint, the top stack frame will then
285: // be selected.
286: parentDelta.addNode(createVMContext(((PDAStartedEvent)e).getDMContext()), IModelDelta.EXPAND | IModelDelta.SELECT);
287: }
288: rm.done();
289: }
290: }
Note: In theory, each VM Node should only generate delta flags that only affect its own elements.  In this way, the layout of the VM Nodes in a view could be customized as needed.  In practice, testing and fine-tuning of the view requires adjustments in the flags returned by the various VM Nodes in a given view.

5 Step 4 - Run Control

Up to this point most of the work in creating the new PDA debugger has gone into infrastructure.  Now it is time to start adding functionality to do some actual debugging.  The work needed to get run control functionality implemented is all encapsulated in the PDARunControl service.

5.1 State Tracking

The primary function of the run control system is to track the current execution state of the program and to issue the corresponding events to the run control service clients.  Both of these tasks are accomplished by the ICommandControl's IEventListner.eventReceived() implementation in combination with service event handlers for IRunControl.IResumedDMEvent and IRunControl.ISuspendedDMEvent.  It may seem odd that the run control service is listening to its own events in order to change its internal state, but doing so guarantees that the execution state reported by the service is consistent with the events it sends out.

org.eclipse.dd.examples.pda.service.PDARunControl - eventReceived()

181:     public void eventReceived(Object output) {
182: if (!(output instanceof String)) return;
183: String event = (String)output;
184:
185: // Handle PDA debugger suspended/resumed events and issue the
186: // corresponding Data Model events. Do not update the state
187: // information until we start dispatching the service events.
188: if (event.startsWith("suspended")) {
189: IDMEvent<?> dmEvent = new SuspendedEvent(fCommandControl.getProgramDMContext(), event);
190: getSession().dispatchEvent(dmEvent, getProperties());
191: } else if (event.startsWith("resumed")) {
192: IDMEvent<?> dmEvent = new ResumedEvent(fCommandControl.getProgramDMContext(), event);
193: getSession().dispatchEvent(dmEvent, getProperties());
194: }
195: }
196:
197:
198: @DsfServiceEventHandler
199: public void eventDispatched(ResumedEvent e) {
200: // This service should be the first to receive the ResumedEvent,
201: // (before any other listeners are called). Here, update the
202: // service state information based on the the resumed event.
203: fSuspended = false;
204: fResumePending = false;
205: fStateChangeReason = e.getReason();
206: fStepping = e.getReason().equals(StateChangeReason.STEP);
207: }
208:
209:
210: @DsfServiceEventHandler
211: public void eventDispatched(SuspendedEvent e) {
212: // This service should be the first to receive the SuspendedEvent also,
213: // (before any other listeners are called). Here, update the
214: // service state information based on the the suspended event.
215: fStateChangeReason = e.getReason();
216: fResumePending = false;
217: fSuspended = true;
218: fStepping = false;
219: }
Note: Much of the sublety in the above logic has to do with preventing race conditions.  Although, while executing in the session executor thread, the state of the system is guaranteed to remain unchanged by other agents, any time a runnable is submitted to the session executor, by the time that runnable is executed, the state of the system may completely change. 

In the above example, a runnable is submitted to the session executor when the DsfSession.dispatchEvent() is called on line 193.  Between the time that the eventReceived() method is completed, and the eventDispatched() methods are called, many clients may query the state of the service.  By delaying the changing of the state until when in the eventDispatch() handlers, the clients are guaranteed to observe the state of the system be consistent with the events received by these clients.

One remarkable aspect of the state tracking logic is the use of the fResumePending flag.  This flag is set to true when a resume or step command is sent to the PDA debugger.  It is then used by the canResume() method to disallow sending another resume command.  This kind of predictive state logic can improve the efficiency of the IDE commands.

org.eclipse.dd.examples.pda.service.PDARunControl - canResume()

222:     public boolean canResume(IExecutionDMContext context) {
223: return isSuspended(context) && !fResumePending;
224: }

5.2 Commands

  All run control commands follow the same pattern shown below, by example of the resume() command:

org.eclipse.dd.examples.pda.service.PDARunControl - resume()

238:     public void resume(IExecutionDMContext context, final RequestMonitor rm) {
239: assert context != null;
240:
241: if (canResume(context)) {
242: fResumePending = true;
243: fCommandControl.queueCommand(
244: new PDAResumeCommand(fCommandControl.getProgramDMContext()),
245: new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
246: @Override
247: protected void handleErrorOrCancel() {
248: // If the resume command failed, we no longer
249: // expect to receive a resumed event.
250: fResumePending = false;
251: super.handleErrorOrCancel();
252: }
253: }
254: );
255: } else {
256: PDAPlugin.failRequest(rm, INVALID_STATE, "Given context: " + context + ", is already running.");
257: }
258: }

5.3 IExecutionDMData

There is very little data that the run control service interface returns for a given execution context.  The intention behind this is to allow the service to remain very generic and thus applicable to any debugger.  In specific debuggers, additional data about processes, threads, cores, etc, should be retrieved from other services or service extensions.  IExpressionDMData is the only object which is retrieved asynchronously from the run control service, and it only contains the state change reason for the last debugger state change. 

6 Step 5 - Breakpoints

Managing breakpoints is one of the complicated tasks that need to be implemented by Eclipse debuggers.  The source of this complexity is the fact that Eclipse breakpoints (IDE breakpoints) are managed independently of breakpoints that are installed in the debugger (target-side breakpoints).  The Eclipse debugger integration has to keep these two sets of breakpoints synchronized. 

6.1 IDE Breakpoints

Eclipse breakpoints are based on markers, which are special tags in the Eclipse resource system and are associated with files and folders.  By using markers, breakpoints gain the benefit of the resource system synchronization mechanism and the automatic persistence.  Also some other cool features such as adjusting the breakpoint line number when the source file is edited, are also automatically gained by using markers. 

An Eclipse breakpoint declaration comes in three parts:

  1. org.eclipse.core.resources.markers extension - A debugger must use this extension to declare a marker type for the new breakpoint.  Markers are hierarchical and a breakpoint should have one of the platform breakpoint objects as a super type.
  2. org.eclipse.debug.core.breakpoints extension - A breakpoint must be declared using this extension point, which requires a valid marker type.
  3. org.eclipse.debug.core.model.IBreakpoint implementation - A breakpoint object must implement this interface.
Finally, in order to have breakpoints appear in the Breakpoints view, they need to be registered with the breakpoint manager, which is represented by the org.eclipse.debug.core.IBreakpointManager interface.  The breakpoint manager also relays events for notifying of breakpoint changes and for a debugger it is the central object used to find the IDE breakpoints that need to be installed on target.

Code listings are omitted here since implementing Eclipse breakpoints is described in fine detail the How to write an Eclipse debugger article.

6.2 Target-Side Breakpoints

DSF defines the IBreakpoints interface for a service which the functions of managing breakpoints installed on the target.  These functions include:

Breakpoint Context

It is expected that primary client of this service is going to be another service which reads IDE Breakpoints, however under certain circumstances such as launching or testing, this interface could be used by other clients, therefore it is desirable that the interface itself should not have any dependencies on the IDE Breakpoint API.  To remain generic, the attributes for breakpoints to be created or update are specified using a simple property bag of type: Map<String, Object>.  After the target-side breakpoint is created, it is represented using an opaque Data Model context of type IBreakpointDMContext.  As an example the PDA line breakpoint context implementation is the following:

org.eclipse.dd.examples.pda.service.PDABreakpoints - BreakpointDMContext

 50:     private static class BreakpointDMContext extends AbstractDMContext implements IBreakpointDMContext {
51:
52: final Integer fLine;
53:
54: public BreakpointDMContext(String sessionId, PDAProgramDMContext commandControlCtx, Integer line) {
55: super(sessionId, new IDMContext[] { commandControlCtx });
56: fLine = line;
57: }
58:
59: @Override
60: public boolean equals(Object obj) {
61: return baseEquals(obj) && (fLine.equals(((BreakpointDMContext) obj).fLine));
62: }
63:
64: @Override
65: public int hashCode() {
66: return baseHashCode() + fLine.hashCode();
67: }
68:
69: @Override
70: public String toString() {
71: return baseToString() + ".breakpoint(" + fLine + ")"; //$NON-NLS-1$//$NON-NLS-2$*/
72: }
73: }

It is also important to note that the IBreakpoints interface is designed to work with debuggers that can track separate breakpoint sets for different cores/processes/threads.  Each context which represents a "breakpoint space" must implement an IBreakpointsTargetDMContext interface, which is then used as a parent context of the IBreakpointDMContext object instances.

Inserting a Breakpoint

PDA debugger supports two types of breakpoints.  The IBreakpoints.insertBreakpoint() implementation performs the task of what type of breakpoint should be installed and delegates to the proper subroutine:

org.eclipse.dd.examples.pda.service.PDABreakpoints - insertBreakpoint()

180:         public void insertBreakpoint(IBreakpointsTargetDMContext context, Map<String, Object> attributes, 
181: DataRequestMonitor<IBreakpointDMContext> rm)
182: {
183: Boolean enabled = (Boolean)attributes.get(IBreakpoint.ENABLED);
184: if (enabled != null && !enabled.booleanValue()) {
185: // If the breakpoint is disabled, just fail the request.
186: PDAPlugin.failRequest(rm, REQUEST_FAILED, "Breakpoint is disabled");
187: } else {
188: String type = (String) attributes.get(ATTR_BREAKPOINT_TYPE);
189:
190: if (PDA_LINE_BREAKPOINT.equals(type)) {
191: // Retrieve the PDA program context from the context given in the
192: // argument. This service is typically only called by the
193: // breakpoints mediator, which was called with the program context
194: // in the services initialization sequence. So checking if
195: // programCtx != null is mostly a formality.
196: PDAProgramDMContext programCtx = DMContexts.getAncestorOfType(context, PDAProgramDMContext.class);
197: if (programCtx != null) {
198: doInsertBreakpoint(programCtx, attributes, rm);
199: } else {
200: PDAPlugin.failRequest(rm, INVALID_HANDLE, "Unknown breakpoint type");
201: }
202: }
203: else if (PDA_WATCHPOINT.equals(type)) {
204: doInsertWatchpoint(attributes, rm);
205: }
206: else {
207: PDAPlugin.failRequest(rm, REQUEST_FAILED, "Unknown breakpoint type");
208: }
209: }
210: }
The doInserBreakpoint() subroutine is listed next:

org.eclipse.dd.examples.pda.service.PDABreakpoints - doInsertBreakpoint()

212:     private void doInsertBreakpoint(PDAProgramDMContext programCtx, final Map<String, Object> attributes, final DataRequestMonitor<IBreakpointDMContext> rm) 
213: {
214: // Compare the program path in the breakpoint with the path in the PDA
215: // program context. Only insert the breakpoint if the program matches.
216: String program = (String)attributes.get(ATTR_PROGRAM_PATH);
217: if (!programCtx.getProgram().equals(program)) {
218: PDAPlugin.failRequest(rm, REQUEST_FAILED, "Invalid file name");
219: return;
220: }

222: // Retrieve the line.
223: Integer line = (Integer)attributes.get(IMarker.LINE_NUMBER);
224: if (line == null) {
225: PDAPlugin.failRequest(rm, REQUEST_FAILED, "No breakpoint line specified");
226: return;
227: }

229: // Create a new breakpoint context object and check that it's not
230: // installed already. PDA can only track a single breakpoint at a
231: // given line, attempting to set the second breakpoint should fail.
232: final BreakpointDMContext breakpointCtx =
233: new BreakpointDMContext(getSession().getId(), fCommandControl.getProgramDMContext(), line);
234: if (fBreakpoints.contains(breakpointCtx)) {
235: PDAPlugin.failRequest(rm, REQUEST_FAILED, "Breakpoint already set");
236: return;
237: }

239: // Add the new breakpoint context to the list of known breakpoints.
240: // Adding it here, before the set command is completed will prevent
241: // a possibility of a second breakpoint being installed in the same
242: // location while this breakpoint is being processed. It will also
243: // allow the breakpoint to be removed or updated even while it is
244: // still being processed here.
245: fBreakpoints.add(breakpointCtx);
246: fCommandControl.queueCommand(
247: new PDASetBreakpointCommand(fCommandControl.getProgramDMContext(), line),
248: new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
249: @Override
250: protected void handleOK() {
251: rm.setData(breakpointCtx);
252: rm.done();
253: }

255: @Override
256: protected void handleErrorOrCancel() {
257: // If inserting of the breakpoint failed, remove it from
258: // the set of installed breakpoints.
259: fBreakpoints.remove(breakpointCtx);
260: super.handleErrorOrCancel();
261: }
262: });
263: }

Removing a Breakpoint

The PDABreakpoints.removeBreakpoint() command takes the IBreakpointDMContext as an argument, but otherwise follows the same general logic as the insertBreakpoint() implementation.

Updating a Breakpoint

Updating a breakpoint involves modifying some of the attributes of an existing breakpoint.  Not all debuggers may support this functionality and for debuggers that do, not all types of breakpoints and not all attributes may be updated.  The IBreakpoints interface does not provide a way for the clients to discover what breakpoints and what attributes may be updated.  It is up to the client to know this information in advance. 

The PDA debugger allows watchpoints to be updated, but only with respect to what operations may trigger the watchpoint:

org.eclipse.dd.examples.pda.service.PDABreakpoints - updateBreakpoint()

367:     public void updateBreakpoint(final IBreakpointDMContext bpCtx, Map<String, Object> attributes, final RequestMonitor rm) {
368: if (!fBreakpoints.contains(bpCtx)) {
369: PDAPlugin.failRequest(rm, REQUEST_FAILED, "Breakpoint not installed");
370: return;
371: }

373: if (bpCtx instanceof BreakpointDMContext) {
374: PDAPlugin.failRequest(rm, NOT_SUPPORTED, "Modifying PDA breakpoints is not supported");
375: } else if (bpCtx instanceof WatchpointDMContext) {
376: WatchpointDMContext wpCtx = (WatchpointDMContext)bpCtx;
377: if (!wpCtx.fFunction.equals(attributes.get(PDAWatchpoint.FUNCTION_NAME)) ||
378: !wpCtx.fVariable.equals(attributes.get(PDAWatchpoint.VAR_NAME)) )
379: {
380: PDAPlugin.failRequest(rm, REQUEST_FAILED, "Cannot modify watchpoint function or variable");
381: return;
382: }
383:
384: // PDA debugger can only track one watchpoint in the same location,
385: // so we can simply remove the existing context from the set and
386: // call insert again.
387: fBreakpoints.remove(bpCtx);
388: doInsertWatchpoint(
389: attributes,
390: new DataRequestMonitor<IBreakpointDMContext>(getExecutor(), rm) {
391: @Override
392: protected void handleOK() {
393: // The inserted watchpoint context will equal the
394: // current context.
395: assert bpCtx.equals(getData());
396: rm.done();
397: }
398: });
399: } else {
400: PDAPlugin.failRequest(rm, INVALID_HANDLE, "Invalid breakpoint");
401: }
402: }

Listing Breakpoints

The PDA Breakpoints service tracks the inserted breakpoints using the fBreakpoints field.  Returning the list of breakpoints only requires returning the current state of this field.

Retrieving Breakpoint Detailed Data

The simple PDA debugger does not track detailed breakpoint data, such has hit counts, addresses etc.  So this function simply returns an error.

6.4 Breakpoints Mediator

With the APIs for managing IDE and Target-Side breakpoints clearly defined, there is a need for a component which will keep the two sets of breakpoint objects synchronized.  DSF provides a standard BreakpointsMediator service to accomplish this task.  

Image 3: Mediating IDE and Target-Side breakpoints
The BreakpointsMediator service is not meant to be sub-classed by specific debugger integrations, however the specific IDE Breakpoint objects as well as Target-Side Breakpoint attributes differ from debugger to debugger.  Therefore, the Breakpoints Mediator requires a helper object, which implements IBreakpointAttributeTranslator which encapsulates the debugger-specific functionality.  The most important function of the attribute translator is to translate IDE Breakpoint attributes into Target-Side breakpoint attributes.  The following listing shows how the PDA debugger implements this function:

org.eclipse.dd.examples.pda.service.PDABreakpointAttributeTranslator - getBreakpointAttributes()

 65:     public List<Map<String, Object>> getBreakpointAttributes(IBreakpoint bp, boolean bpManagerEnabled) 
66: throws CoreException
67: {
68: Map<String, Object> attrs = new HashMap<String, Object>();

70: // Check that the marker exists and retrieve its attributes.
71: // Due to accepted race conditions, the breakpiont marker may become null
72: // while this method is being invoked. In this case throw an exception
73: // and let the caller handle it.
74: IMarker marker = bp.getMarker();
75: if (marker == null || !marker.exists()) {
76: throw new DebugException(new Status(IStatus.ERROR, PDAPlugin.PLUGIN_ID, DebugException.REQUEST_FAILED, "Breakpoint marker does not exist", null));
77: }
78: // Suppress cast warning: platform is still on Java 1.3
79: @SuppressWarnings("unchecked")
80: Map<String, Object> platformBpAttrs = marker.getAttributes();

82: // Copy breakpoint attributes.
83: if (bp instanceof PDAWatchpoint) {
84: attrs.put(PDABreakpoints.ATTR_BREAKPOINT_TYPE, PDABreakpoints.PDA_WATCHPOINT);

86: copyAttributes(platformBpAttrs, attrs, fgPDAWatchpointAttributes);
87: } else if (bp instanceof PDALineBreakpoint) {
88: attrs.put(PDABreakpoints.ATTR_BREAKPOINT_TYPE, PDABreakpoints.PDA_LINE_BREAKPOINT);
89: attrs.put(PDABreakpoints.ATTR_PROGRAM_PATH, marker.getResource().getFullPath().toString());

91: copyAttributes(platformBpAttrs, attrs, fgPDALineBreakpointAttributes);
92: }

94: // If the breakpoint manager is disabled, override the enabled attribute.
95: if (!bpManagerEnabled) {
96: attrs.put(IBreakpoint.ENABLED, false);
97: }

99: // The breakpoint mediator allows for multiple target-side breakpoints
100: // to be created for each IDE breakpoint. Although in case of PDA this
101: // feature is never used, we still have to return a list of attributes.
102: List<Map<String, Object>> retVal = new ArrayList<Map<String, Object>>(1);
103: retVal.add(attrs);
104: return retVal;
105: }

107: private void copyAttributes(Map<String, Object> srcMap, Map<String, Object> destMap, String[] attrs) {
108: for (String attr : attrs) {
109: if (srcMap.containsKey(attr)) {
110: destMap.put(attr, srcMap.get(attr));
111: }
112: }
113: }

7 Step 6 - Stack

7.1 Command Cache

A new feature introduced in this service is the command cache.  The command cache is a surprisingly simple mechanism for caching service data which otherwise would need to be retrieved repeatedly from the debugger back end.  The command cache performs two functions:
  1. Map Command objects to corresponding Command Result objects.
  2. While waiting for a Command to be processed, queue incoming Commands for the same data and complete them when the first Command is completed.
Using the command cache greatly simplifies the logic in implementing data retrieval commands.  As an example, the following is the listing of the PDAStack.getFrames() method:

org.eclipse.dd.examples.pda.service.PDAStack - getFrames()

244:     public void getFrames(IDMContext context, final DataRequestMonitor<IFrameDMContext[]> rm) {
245: // Can only create stack frames for an execution context as a parent,
246: // however the argument context is a generic context type, so it could
247: // be an execution context, a frame, a variable, etc. Search the
248: // hierarchy of the argument context to find the execution one.
249: final IExecutionDMContext execCtx = DMContexts.getAncestorOfType(context, IExecutionDMContext.class);
250: if (execCtx == null) {
251: PDAPlugin.failRequest(rm, IDsfService.INVALID_HANDLE, "Invalid context " + context);
252: return;
253: }

255: // Execute the stack command and create the corresponding frame contexts.
256: fCommandCache.execute(
257: new PDAStackCommand(fCommandControl.getProgramDMContext()),
258: new DataRequestMonitor<PDAStackCommandResult>(getExecutor(), rm) {
259: @Override
260: protected void handleOK() {
261: IFrameDMContext[] frameCtxs = new IFrameDMContext[getData().fFrames.length];
262: for (int i = 0; i < getData().fFrames.length; i++) {
263: frameCtxs[i] = new FrameDMContext(getSession().getId(), execCtx, i);
264: }
265: rm.setData(frameCtxs);
266: rm.done();
267: }
268: });
269: }

7.2 Frame Context

The primary object type managed by the stack service a stack frame.  It is implemented by the FrameDMContext object listed below:

org.eclipse.dd.examples.pda.service.PDAStack - FrameDMContext

 54:     private static class FrameDMContext extends AbstractDMContext implements IFrameDMContext {

56: final private int fLevel;

58: FrameDMContext(String sessionId, IExecutionDMContext execDmc, int level) {
59: super(sessionId, new IDMContext[] { execDmc });
60: fLevel = level;
61: }

63: public int getLevel() { return fLevel; }

65: @Override
66: public boolean equals(Object other) {
67: return super.baseEquals(other) && ((FrameDMContext)other).fLevel == fLevel;
68: }

70: @Override
71: public int hashCode() {
72: return super.baseHashCode() ^ fLevel;
73: }

75: @Override
76: public String toString() {
77: return baseToString() + ".frame[" + fLevel + "]"; //$NON-NLS-1$ //$NON-NLS-2$
78: }
79: }
The frame context, which in itself does not provide access to much frame information, can be used to retrieve more complete frame data implemented using the FrameDMData object.  The frame data object is based on information parsed from the result of the PDA debugger "stack" command, which is written into the PDAFrame object by the PDAStackCommandResult

org.eclipse.dd.examples.pda.service.PDAStack - FrameDMData

 85:     private static class FrameDMData implements IFrameDMData {

87: final private PDAFrame fFrame;

89: FrameDMData(PDAFrame frame) {
90: fFrame = frame;
91: }

93: public String getFile() {
94: return fFrame.fFilePath.lastSegment();
95: }

97: public String getFunction() {
98: return fFrame.fFunction;
99: }

101: public int getLine() {
102: return fFrame.fLine + 1;
103: }

105: public int getColumn() {
106: return 0;
107: }

109: public IAddress getAddress() {
110: return null;
111: }
112: }

8 Step 7 - Source Display

Integrating source display is relatively simple task, even though the Eclipse APIs for looking up and displaying source are rather convoluted.  Fortunately Eclipse Platform and DSF provide most of the components needed to implement this functionality.

Image 3: Components involved in Source Display
The first step in integrating source lookup, is to create a source lookup director and the source container.  This is explained in detail in the How to write an Eclipse debugger article referenced earlier.  There is one difference in this process for a DSF-based debugger.  The source lookup director does not need to initialize a source lookup participant list.

The second step is to register a DSF source display adapter.  This has actually already been accomplished in Step 3 - View Model, when registering the View Model adapters.

9 Step 8 - Variables

At this point we almost have a fully functional PDA debugger.  The only thing that is left is to populate the Variables and Expressions views. 
Displaying variable data in DSF requires use of two services:
  1. IStack service - This service is used to get the list of local variables' names in a given stack frame.
  2. IExpressions service - This service is used to evaluate the variable names as expressions in order to retrieve full type and value information for the given variables.

9.1 Variable Contexts

The stack service allows clients to retrieve two types of variables through two methods
Both of these methods return a context that implements the IStack.IVariableDMContext interface.  The implementation of this context is very simple in PDA

org.eclipse.dd.examples.pda.service.PDAStack - VariableDMContext

117:     @Immutable
118: private static class VariableDMContext extends AbstractDMContext implements IVariableDMContext {

120: final private String fVariable;

122: VariableDMContext(String sessionId, IFrameDMContext frameCtx, String variable) {
123: super(sessionId, new IDMContext[] { frameCtx });
124: fVariable = variable;
125: }

127: String getVariable() { return fVariable; }

129: @Override
130: public boolean equals(Object other) {
131: return super.baseEquals(other) && ((VariableDMContext)other).fVariable.equals(fVariable);
132: }

134: @Override
135: public int hashCode() {
136: return super.baseHashCode() + fVariable.hashCode();
137: }

139: @Override
140: public String toString() {
141: return baseToString() + ".variable(" + fVariable + ")"; //$NON-NLS-1$ //$NON-NLS-2$
142: }
143: }
The getLocals() implementation is also very simple as it only uses the familiar stack PDA debugger command:

org.eclipse.dd.examples.pda.service.PDAStack - getLocals()

271:     public void getLocals(final IFrameDMContext frameCtx, final DataRequestMonitor<IVariableDMContext[]> rm) {
272: // Execute the stack command again.
273: fCommandCache.execute(
274: new PDAStackCommand(fCommandControl.getProgramDMContext()),
275: new DataRequestMonitor<PDAStackCommandResult>(getExecutor(), rm) {
276: @Override
277: protected void handleOK() {
278: // Find the correct PDAFrame
279: int frameId = getData().fFrames.length - frameCtx.getLevel() - 1;
280: if (frameId < 0) {
281: PDAPlugin.failRequest(rm, IDsfService.INVALID_HANDLE, "Invalid frame level " + frameCtx);
282: return;
283: }
284: PDAFrame pdaFrame = getData().fFrames[frameId];

286: // Create variable contexts for all variables in frame.
287: IVariableDMContext[] variableCtxs = new IVariableDMContext[pdaFrame.fVariables.length];
288: for (int i = 0; i < pdaFrame.fVariables.length; i++) {
289: variableCtxs[i] = new VariableDMContext(getSession().getId(), frameCtx, pdaFrame.fVariables[i]);
290: }
291: rm.setData(variableCtxs);
292: rm.done();
293: }
294: });

296: }
The IStack interface also defines an IVariableDMData interface which returns information about the variable.  However, the only method from this interface used by the Variables view is the getName() method and it is the only method implemented by the PDA debugger integration:

9.2 Expression Contexts

The IExpressions interface uses a somewhat unusual way of managing contexts.  A client can create an IExpressionDMContext context instance for any expression and parent context by calling the IExpressions.createExpressions() method.  However, when the returned expression context is evaluated, if its expression is not valid or if it uses an invalid parent context, it is guaranteed to fail.  The PDAExpressions.createExpression() implementation reflects this:

org.eclipse.dd.examples.pda.service.PDAExpressions - createExpression()

213:     public IExpressionDMContext createExpression(IDMContext ctx, String expression) {
214: // Create an expression based on the given context and string expression.
215: // The PDA debugger can only evaluate variables as expressions and only
216: // in context of a frame.
217: IFrameDMContext frameCtx = DMContexts.getAncestorOfType(ctx, IFrameDMContext.class);
218: if (frameCtx != null) {
219: return new ExpressionDMContext(getSession().getId(), frameCtx, expression);
220: } else {
221: // If a frame cannot be found in context, return an "invalid"
222: // expression context, because a null return value is not allowed.
223: // Evaluating an invalid expression context will always yield an
224: // error.
225: return new InvalidExpressionDMContext(getSession().getId(), ctx, expression);
226: }
227: }

9.3 Expression Data

The IExpressions interface allows for retrieving data in two stages:
  1. Variable type and other data.
  2. Variable value.
The implementation of the IExpressions.IExpressionDMData interface is required by the Variables views, however for the PDA debugger the IExpressionDMData does not carry any additional information beyond the expression string.

9.4 Formatted Value Context

The IExpressions service allows expression values to be retrieved in different, client-selectable formats.  For the PDA debugger, only one formatting is available, which is reflected in the implementation of IFormattedValues.getAvailableFormats() method.   Also, the implementation of the IFormattedValues.getFormattedValueContext() method is just a formality.

org.eclipse.dd.examples.pda.service.PDAExpressions - getAvailableFormats()

262:     public void getAvailableFormats(IFormattedDataDMContext dmc, DataRequestMonitor<String[]> rm) {
263: // PDA debugger doesn't support formatting the expression. Natural
264: // formatting is the only available option.
265: rm.setData(new String[] { NATURAL_FORMAT });
266: rm.done();
267: }

269: public FormattedValueDMContext getFormattedValueContext(IFormattedDataDMContext exprCtx, String formatId) {
270: // Creates a context that can be used to retrieve a formatted value.
271: return new FormattedValueDMContext(this, exprCtx, formatId);
272: }

Only when the formatted value context is evaluated, the var command is sent to the PDA debugger:

org.eclipse.dd.examples.pda.service.PDAExpressions - getFormattedExpressionValue()

274:     public void getFormattedExpressionValue(FormattedValueDMContext formattedCtx, 
275: final DataRequestMonitor<FormattedValueDMData> rm)
276: {
277: final ExpressionDMContext exprCtx = DMContexts.getAncestorOfType(formattedCtx, ExpressionDMContext.class);
278: if (exprCtx != null) {
279: final IFrameDMContext frameCtx = DMContexts.getAncestorOfType(exprCtx, IFrameDMContext.class);
280:
281: // First retrieve the stack depth, needed to properly calculate
282: // the frame index that is used by the PDAVarCommand.
283: fStack.getStackDepth(
284: frameCtx, 0,
285: new DataRequestMonitor<Integer>(getExecutor(), rm) {
286: @Override
287: protected void handleOK() {
288: // Calculate the frame index.
289: int frameId = getData() - frameCtx.getLevel() - 1;
290:
291: // Send the command to evaluate the variable.
292: fCommandCache.execute(
293: new PDAVarCommand(fCommandControl.getProgramDMContext(), frameId, exprCtx.getExpression()),
294: new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
295: @Override
296: protected void handleOK() {
297: rm.setData(new FormattedValueDMData(getData().fResponseText));
298: rm.done();
299: }
300: });
301: }
302: });
303: } else {
304: PDAPlugin.failRequest(rm, INVALID_HANDLE, "Invalid expression context " + formattedCtx);
305: rm.done();
306: }
307: }

10 Additional Resources

10.1 Debug Platform

The platform team has evolved and improved the debug APIs over many releases.  Before starting a debugger integration using DSF, it would be very helpful to familiarize yourself with materials in these documents:

10.2 GDB

The DSDP Device Debugging project is developing a DSF-based debugger integration with GDB as the main reference implementation of the DSF APIs.  In comparison with PDA, GDB is a much more complex and feature rich debugger integration.  The GDB debugger integration is also being continually developed and improve features and performance.

10.3 TCF

The Target Communication Framework which is being developed as part of the DSDP Target Management project includes a reference debugger agent implementation. This reference debugger agent is integrated with Eclipse using DSF.  The TCF debugger is more limited in features than the GDB debugger, however the TCF debugger API is considerably different than the GDB/MI protocol and is an interesting example integration.