org.openide.debugger
.
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.
Debugger
abstract class. It must provide the following sets of features:
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.
Debugger.createBreakpoint(...)
(and related methods) or
Debugger.createWatch(...)
.
The debugger must create an implementation
of these objects.
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 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.
GoAction
,
FinishDebuggerAction
,
TraceIntoAction
,
TraceOverAction
,
and
StepOutAction
.
These actions essentially just use the basic interface presented in
the Debugger
interface.
AddWatchAction
is also implemented in the APIs and uses the debugger's default method of adding watches.
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.
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.
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:
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.
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.
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).
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.
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.
If you wish to provide a window (possibly multitabbed) showing
your debugging nodes, the way the standard module does, you can
create
ExplorerPanel
s
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.
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.
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
Executor
s)
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:
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.
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.
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.
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.
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.