Overview

Javadoc

The Javadoc resides in org.openide.debugger.

Contents

Debugger API

The Debugger API provides a thin interface between the IDE and various possible debugger implementations, including the IDE's standard debugger. The API is designed to work with Java programs, although it could also be used for any language compiled to the JVM.

Note that the API itself defines only the basic methods of communication, and does not specify a rich level of functionality. However, what it provides is sufficient to implement all of the connections between the standard debugger module and the debugging-related actions and UI components, including with the Editor. Real debuggers will very likely have more features than are specified here, but it is not difficult to make the added functionality visible to the user.

There are two different aspects to adding debugging capabilities to the IDE. One is to create the debugger itself, i.e. a set of implementations which specify how the debugger will be run, its current dynamic status, manipulation of breakpoints and watches, etc. The other half is to create debugger types which specify how the debugger should be invoked for particular types of objects - for example, applets need to be debugged by means of launching the AppletViewer pointing to the correct applet class or HTML page; but once the AppletViewer's main class has been launched with the correct arguments, the remaining implementation is entirely up to the debugger. These two halves can (and typically are) treated quite separately, so that the standard IDE debugger module installs a debugger as well as a couple of simple, generic debugger types, while modules providing support for more exotic data types such as applets or servlets provide their own specialized debugger types.

Creating a Debugger

To create a debugger, you must implement the Debugger abstract class. It must provide the following sets of features:
  1. Access to, and control of, the debugger's basic state. The state should be queriable by means of Debugger.getState(). Initially, the debugger will be Debugger.DEBUGGER_NOT_RUNNING When some other part of the system starts it up with Debugger.startDebugger(...), it may indicate its state during the initialization phase as Debugger.DEBUGGER_STARTING. When it is ready, it will be either Debugger.DEBUGGER_RUNNING (if actually executing user code at the time), or Debugger.DEBUGGER_STOPPED (if stopped at a breakpoint).

    Once it is running, the debugger should respond to the basic commands Debugger.traceInto(), Debugger.traceOver(), and Debugger.stepOut(), all of which return it to the stopped state; and Debugger.go(), putting it into the running state (unless a breakpoint is encountered). Debugger.finishDebugger() should stop the debugger as soon as possible (some cleanup may be required) and leave it in the not-running state.

    The DebuggerInfo provides the basic information needed for the debugger to start - the name of the main class; a list of "command-line" parameters to pass to its main(String[]) method, which may be empty; and (optionally) the name of a class to first stop execution on (as if there were breakpoints on all of its methods).

    The debugger can look in the repository to find source code; use the user class loader to load user classes (into the IDE process); and for a normal external debugger, determine how to prepare the classpath correctly, which generally involves scanning through all filesystems, asking each to add information to an object supplied by the debugger implementing FileSystem.Environment. If classes should be compiled before being used in the debugger, the debugger should take care of that using the Compiler API.

  2. The debugger should be able to create breakpoints and watches when requested by Debugger.createBreakpoint(...) (and related methods) or Debugger.createWatch(...). The debugger must create an implementation of these objects.
  3. The debugger should fire events for a few properties, so that other parts of the system (such as the Trace Over action) can be sensitive to changes in the debugger's state.
  4. Most debuggers (including the standard module) will need to provide certain options not specified in the API, such as whether to compile automatically before debugging; extra class path elements to use; parameters to connect to an external debugger; etc. These should be handled by providing a system option in the debugger's module.

Special breakpoint & watch implementation

The debugger is not required to do anything for implementation of breakpoints and watches beyond the small interfaces specified in Breakpoint and Watch, which essentially only specify abstract points in debugged code and request that listeners be notified of changes in these basic properties. These objects need only be created by the main debugger class, as mentioned above.

However, it is likely that the debugger implementation will want to provide additional functionality, possibly pertaining to these objects. For example:

None of this interferes with the use of the API - the only likely caveat is that the Editor might expect a watch to operate correctly when its expression is an unqualified (local or member) variable name, so this should be considered. (This most common usage of watches is the one that a user will expect anyway.)

Any additional functionality built onto the debugger itself, or onto its breakpoints or watches, ought to be exposed as JavaBeans properties, and events fired upon their change - this convention ensures that if such objects are used by any other part of the system (say, inspected in a property sheet, customized in some action...), that the extended functionality will be properly presented. See the section on extra implementation for ideas on how to present additional functionality.

Installation tips

Installation of the debugger itself is quite simple thanks to the Services API - you need only register an instance of it to lookup

A typical debugger implementation will, however, want to install some other components to support it, such as a system option (mentioned above), or an environment node to represent the state of the running debugger, its breakpoints and watches, possibly threads, etc. - see the Nodes API for instructions on doing this.

Creating Extra Debugger-Related Implementation

While the previous section described what is required of the debugger by the APIs, in practice a good debugger module will present more of a user interface than the APIs specify. By default, the implementation of the IDE does not do much with the debugger directly:
  1. Various actions are present and implemented in the APIs, which control the global state of the debugger in basic ways. These include GoAction, FinishDebuggerAction, TraceIntoAction, TraceOverAction, and StepOutAction. These actions essentially just use the basic interface presented in the Debugger interface.
  2. AddWatchAction is also implemented in the APIs and uses the debugger's default method of adding watches.
  3. ToggleBreakpointAction is actually implemented currently in the Java loader module, but effectively the debugger author need not worry about it - again, it uses the debugger's default method of adding and removing breakpoints.
  4. DebuggerType.Default is available as a bare-bones debugger type which just invokes the debugger via the public interface; you may want to write your own debugger types to complement it.
The following subsections describe things which the IDE implementation does not do automatically, and ideas on how to implement them suitably for your debugger. They are all optional for a compliant debugger implementation, since they affect the UI rather than the interactions of other parts of the IDE with the debugger; most are desirable nevertheless. Writing debugger types is also a good idea, but is described separately since it may also be done in modules outside of the debugger proper, as debugger types are specific to certain kinds of development support (e.g. RMI, servlet, etc.).

Handling the current line

It is desirable (though not strictly required) that a debugger update the Editor to track the current line as it moves through source code (if such movement can be tracked properly, and the current source is available). This is the debugger's responsibility, not the Editor's nor the system's.

Though different debuggers may of course vary in exactly how they wish to do this, the basic implementation is not difficult once you know where to look:

  1. Keep track of the current line in a variable of type Line, which is designed for exactly this sort of purpose. You may use Line.markCurrentLine() and Line.unmarkCurrentLine() whenever control enters or exits this line.
  2. To find the current source file based on the classname, Repository.find(...) is easiest. You may instead wish to get all file systems marked as supporting debugging via FileSystemCapability.DEBUG and then call FileSystemCapability.find(...) to find the source file within these file systems only.
  3. Knowing the file containing source, the lines in it are available by first getting the data object (with DataObject.find(...)), then using DataObject.getCookie(...) to look for a LineCookie (which ought to have been provided by, e.g., the EditorSupport attached by the standard Java loader), and finally getting the desired line with LineCookie.getLineSet() and Line.Set.getOriginal(int) (which finds the line based on the original line numbering, even if the user has since edited the document).

Adding extra actions

Many debuggers will provide additional functionality as regards their running state over that specified in the APIs. For example, the standard IDE debugger supports suspending and resuming the debugger, which is not required by the APIs and not accessible via them. For this reason, you may want to create your own user-visible actions to support extended debugger manipulation using the Actions API.

The IDE's default menu and toolbar configurations should include the basic (API-supplied) actions in sensible positions. If you do not agree with these positions, or wish to add your own special actions to the menus or toolbars, the Actions API again describes how to do this.

Finally, you may want to provide context-menu actions on any nodes which you create for your debugger; if so, please use the Nodes API for guidance.

Adding nodes to "Runtime"

The standard debugger installs a new master node into the Runtime tab in the Explorer, containing lists of current breakpoints and watches, as well as extra functionality it has such as current threads (and thread groups), and debugging sessions in the case of a multiple-session debugger. Debugger implementors who wish to provide similar functionality should do so themselves; although there is no direct API support for this, in practice creating basic lists of objects is not difficult.

The basic idea is to use the Nodes API to create simple lists of all objects in your debugger's current state. For starters, you could create a master node (the one directly under Runtime) as an AbstractNode using Children.Array: the children can just be listed as is, since you will want one child for each category (breakpoints, watches, etc.).

Now, each category node (e.g. list of all watches) can again be an AbstractNode, but this time using Children.Keys so that its children are dynamically determined; in the Children.addNotify() method, call Children.Keys.setKeys(Collection) to update the list of keys. Probably you will want each key to just be one actual watch object from the debugger. Also, attach a listener to your debugger so that when the set of watches changes, setKeys will be called again with the new list of children to update the display. Note that if your implementation supports hidden breakpoints, as determined by Breakpoint.isHidden(), then these should of course be excluded from the visible list.

In the Children.Keys.createNodes(Object) method, you know your key is a watch object, so just use new BeanNode(Object) to represent it. Remember, watches and breakpoints should have bound properties and bean info just like any JavaBean, so using a BeanNode for them should provide all the right behavior automatically. You can also subclass BeanNode and implement BeanNode.canDestroy() and BeanNode.destroy() (etc.) to permit the user to delete watches from this view - the method should simply remove the watch from the debugger, and then the parent node will get routinely notified via its listener of the change and refresh the display.

Other types of objects such as threads and so on can also be represented in similar ways, but of course the details of how to set up the nodes will vary depending on how you represent such objects; refer to the Nodes API for this.

Finally, the master node can be installed by your module using the Modules API.

Adding a debugging workspace, windows, etc.

You can check how the IDE looks without your debugger module (or the standard debugger module) installed and then decide if you need to customize workspaces, windows, and so on. If you wish, you can create your own customized workspaces and so on for the purpose of giving a characteristic feel and UI to a user's debugging sessions; refer to the Window System API for all details.

If you wish to provide a window (possibly multitabbed) showing your debugging nodes, the way the standard module does, you can create ExplorerPanels to hold e.g. a BeanTreeView and a PropertySheetView, then assign the root node to such a window using ExplorerPanel.getExplorerManager() and ExplorerManager.setRootContext(Node). The multitabbed look can be achieved simply using window manager modes.

Adding a system option

You will probably want to add a system option to the IDE representing general configuration of your debugger (specifics of how to debug particular files, such as the path to any required debugger executable, ought to be left to debugger types). Naturally what goes into this option is up to you, but it is recommended that it at least provide user-visible configuration for two static properties defined in the APIs: StartDebuggerAction.getRunCompilation() (StartDebuggerAction.setRunCompilation(boolean)) and StartDebuggerAction.getWorkspace() (StartDebuggerAction.setWorkspace(String)). Note that for the latter you may wish to set a default to your customized workspace at install time.

Persisting debugger state

Finally, it is important for the user experience of the IDE that as much as possible of their environment persist across IDE restarts. This is generally done by storing various types of configurations in the project state. In the case of debuggers, conventionally at least the list of breakpoints and watches should be stored, so that a user does not need to set up their debugging environment afresh each time the IDE is restarted. Fortunately, implementing this is rather easy; it is suggested that you:
  1. Make sure your watches and breakpoints are really safely serializable (as required by the API interfaces).
  2. Create a bean holding the set of watches and breakpoints as properties. It should be serializable and fire property changes.
  3. Register an instance of this bean to lookup. It need not have any visible representation.
  4. You can now ask lookup for the instance of the bean, and changes should be saved to disk automatically.

Creating a Debugger Type

You may create debugger types in order to provide special support for debugging objects other than simple Java classes with main methods. For example, debugging applets may entail launching the debugger on the AppletViewer application, passing in the HTML URL for the applet to be debugged. (For classes with main methods, the standard default implementation DebuggerType.Default suffices.) Thus, debugger types are the bridge between the specifics of how a file should be started (in this way resembling Executors) and the debugger implementation (which assumes that it is being started on a class with a main method).

Creating and installing the debugger type with its associated configuration is a fairly uniform process, according to the Services API, since the required superclass DebuggerType is a variety of ServiceType. Only issues pertaining specifically to subclassing DebuggerType will be discussed here.

There is just one abstract method to implement: DebuggerType.startDebugger(ExecInfo,boolean). When a user of debugger types (such as ExecSupport, see below) wishes to start debugging some object (say, an applet), it will call this method on the debugger type, passing it an ExecInfo containing the class name to be debugged (and possibly additional application-level arguments, treated e.g. as parameters for a main method); and a flag indicating whether the debugger should initially break on the "main class" of the object (however that should be interpreted).

Typically, the debugger type will do a few simple steps:

  1. Construct a new DebuggerInfo(...) according to the information passed in the ExecInfo, as well as the specifics of the debugger type. For example, in the case of applets, the DebuggerInfo should specify the main class as being that of the AppletViewer application, and the class name of the applet should be used to look up an HTML page which can then be passed in the argument list along with other options.
  2. Handle the stop-on-main flag appropriately. If the flag is clear, typically
    new DebuggerInfo (mainClass, arguments, null)
    
    should be used, to prevent the debugger from breaking except at user-specified points. If the flag is set, you may use the two-argument constructor, but this will break at the beginning of the real main class (e.g. AppletViewer); you may instead prefer to explicitly specify the class to break on, such as the applet class.
  3. Actually launch the debugger with:
    TopManager.getDefault ().getDebugger ().startDebugger (myDebugInfo);
    

Note that general problems occurring during this problem can be reported with a DebuggerException.

You have the option of what exactly to include in the DebuggerInfo option passed to the startDebugger method. If the debugger type is to be generic (work with any installed debugger), then you should only assume the API minimum and use an actual instance of DebuggerInfo. But, it is permissible to provide additional debugger-specific information, if that information can be used to good effect by the debugger implementation.

For example, the standard IDE debugger actually defines several additional subclasses of DebuggerInfo and provides a special API to these (outside of the Open APIs), such as an extended info which contains descriptions of the exact process name to be used to start the Java launcher in debug mode, special VM options, and so on. It also defines standard debugger-type superclasses which already have all of this configuration present as Bean properties. So, debugger types (such as those for applets) can specify an applet style of debugging, along with special VM characteristics for the user to configure. If the debugger is handed a plain DebuggerInfo, it simply uses default settings for all the specifics; conversely, an extended DebuggerInfo could be passed to an alternate debugger implementation without harm, as the alternate debugger would just not recognize the extra information in the subclass.

Invoking the Debugger

There is little to say about how to invoke an existing debugger programmatically - the essential methods are all provided in the Debugger interface, and the debugger can be started with only a small amount of information. The debugger instance itself should be obtained via TopManager.getDebugger() (currently the system expects only one debugger to be installed at a time; the APIs permit the implementation to keep multiple debuggers loaded with only one active at a time, though currently the implementation does not do this).

You may also wish to start debugging a particular file by means of its DebuggerCookie; most frequently this implementation is an ExecSupport which will delegate the request to an associated debugger type, which then launches the system debugger with the correct arguments. This more natural method is that used by the standard debugger-launching actions.

The Services API also permits you to look for specific debugger types which could then be applied manually to a specific file.

Using hidden watches

A sophisticated editor implementation, and potentially other components as well, may want to monitor the current value of a variable mentioned in source code (e.g.) while the debugger is in operation. This is possible using a hidden watch, which functions exactly like a regular watch but is not created by the user and not intended for direct display to the user. To create one, just get the debugger and call Debugger.createWatch(...) (with the second argument true). Typically the watched expression will be an unqualified variable name, but of course this depends on the watch's use, and what the debugger implementation is capable of handling (only assume bare variable names are supported).

Now, the current display value of the watch is available with Watch.getAsText(), and the corresponding property may be listened to in order to update some user-oriented display (such as a tool tip).

Remember to call Watch.remove() as soon as the watch is no longer required, so as not to waste memory.


Built on December 12 2001.  |  Portions Copyright 1997-2001 Sun Microsystems, Inc. All rights reserved.