How do I listen to Debugger events within my own intellij Java/Gradle plugin?

Answered

First, I want to explain "why" - 'what is the goal I want to achieve'
I am currently working in Java/Kotlin code bases that are very complex; written in a very "spaghetti-code" way.

I often find myself stepping through this code, observing the values that show up under the "variables" section of the debug panel.

So, in order to help make this process easier, I want to write a plugin, that will be able to keep track of two main things:
- Keep track of a sequence of method names, in chronological order of execution
- Keep track of each of the variables and their values that were in-scope within each of these methods

What I will do is then use the method names and variable values to build a diagram where each box in the diagram is a method. When I hover over the method, I will be able to see what variable/values were in scope upon entering and exiting the method.

How am I attempting to reach this goal (above)?

I want to listen to debugger events. So that every time I hit a breakpoint, my plugin will execute code that will record:
- The function in which the breakpoint was hit
- The current variable/values that are in scope when the breakpoint was hit

I know that "projectListeners" and "applicationListeners" can be defined inside a plugin.xml file. Then the intellij plugin can have a class that @Override annotates a method so that this override method 'listens' to when the original method would get hit.

The information I'm looking for is this:
- Can you provide a complete example with the plugin.xml and the related class @Override method. This example would display a "Hello World" message whenever a breakpoint is hit in an intellij editor that has the plugin loaded
- If it's not possible to 'listen' to the debugger event in this way, could you provide a complete code example, that accomplishes showing a "Hello World" message when a breakpoint is hit, using a strategy that will work?

Thank you very much to whoever is able to help me, as a brand new intellij plugin developer.

cheers!

15 comments
Comment actions Permalink

Hi, you can detect a new debug session adding a new XDebuggerManager listener:

project.messageBus.connect(lifetime).subscribe(XDebuggerManager.TOPIC, object : XDebuggerManagerListener {
override fun processStarted(debugProcess: XDebugProcess) {...}}

Then inside you can add a session listener:

debugProcess.session.addSessionListener(object : XDebugSessionListener {override fun sessionPaused() {...}})

This listener is called every time the debugger is stopped on a breakpoint or after a step. When session is stopped, you can get the current frame with session.getCurrentStackFrame(); and then collect visible variables there using the computeChildren method.

 

0
Comment actions Permalink

If you wouldn't mind answering another question...

This first part works like a charm, thank you for pointing me in the right direction towards subscribing to debugger events; The code that I have is correctly firing events

- Debugger started event fires correctly

- Debugger paused event fires correctly

 

I still have a question about the computeChildren method.

I am trying to get the variable values, but it looks like those values may be calculated asynchronously? 

 

Here is what I have so far in Java:

 

public class MyPostStartupActivity implements StartupActivity {

@Override
public void runActivity(@NotNull Project project) {
attachDebugStartListener(project);
}

private void attachDebugStartListener(Project project) {
project.getMessageBus().connect().subscribe(XDebuggerManager.TOPIC, new XDebuggerManagerListener() {
@Override
public void processStarted(@NotNull XDebugProcess debugProcess) {
attachDebugBreakListener(debugProcess);
}
});
}

private void attachDebugBreakListener(@NotNull XDebugProcess debugProcess) {
debugProcess.getSession().addSessionListener(new XDebugSessionListener() {
@Override
public void sessionPaused() {
attachComputeChildrenListener(debugProcess.getSession().getCurrentStackFrame());
}
});
}

private void attachComputeChildrenListener(XStackFrame currentStackFrame) {
currentStackFrame.computeChildren(new XCompositeNode() {
@Override
public void addChildren(@NotNull XValueChildrenList children, boolean last) {
for(int c = 0; c < children.size(); c++) {
XValue childValue = children.getValue(c);

// TODO get a variable value from childValue
}
}

...
});
}
}

 

computeChildren requires a new XCompositeNode() { ... } parameter. So I let intellij help me find out what override methods were available, including a method called addChildren

 

addChildren seems like it could be called whenever the children (variables) are set to the variables panel in the debugger. 

 

XValue childValue is an object that contains the "name" of the variable. However, I don't see the value loaded in this object. I just see the variable name.

 

Do I have to call a different function on childValue, in order to wait for the value to load, or is there a different way I should be using computeChildren to get the values? 

 

Would you mind showing me a sample of how the variable values can be loaded with computeChildren

 

Cheers!

0
Comment actions Permalink
And this is what I'm trying to get in the code:
  • All variable names in the frame (done)
  • All variable values in the frame (this is what I'm asking for help with)
  • The name of the function where execution is paused (this is what I'm asking for help with)

0
Comment actions Permalink

Just revisiting this to see if I can find out how to retrieve debugger values by looking at the platform.api.jar...

I see in this file:
com\intellij\xdebugger\frame\XCompositeNode.java

It looks like it says:

Represents a node with children in a debugger tree. This interface isn't supposed to be implemented by a plugin.


Still seeing if I can find a way to get debugger values. 

 

Any examples for retrieving debugger variable values from the currentStackFrame are greatly appreciated

0
Comment actions Permalink

XStackFrame is a container of local variables, to collect them (async) you need to call computeChildren, for an example see the implementation of com.intellij.xdebugger.XDebuggerTestUtil#collectChildren (https://github.com/JetBrains/intellij-community/blob/992017ff8b554cf51b67110e6d30a402aaefd1ab/platform/xdebugger-testFramework/src/com/intellij/xdebugger/XDebuggerTestUtil.java#L180).

You'll obtain a List of XValue objects, from there you'll be able to obtain a representation of the values using computePresentation, for an example see https://github.com/JetBrains/intellij-community/blob/992017ff8b554cf51b67110e6d30a402aaefd1ab/platform/xdebugger-testFramework/src/com/intellij/xdebugger/XDebuggerTestUtil.java#L273

0
Comment actions Permalink

Thank you for your patience. 

I am having difficult understanding how I can use the example code inside my plugin. 

In the example, I see there is an XValueContainer.computeChildren (line 196).

Alternately, in my code, I have an XStackFrame.computeChildren. I see that XStackFrame extends XValueContainer and also has a computeChildren method. 

XStackFrame.computeChildren seems to require a parameter of type XCompositeNode.

Alternatively, the example instantiates an XTestCompositeNode to pass to the XValueContainer.computeChildren method. 

I'm not able to instantiate XCompositeNode in the same way as XTestCompositeNode since XCompositeNode requires the full XCompositeNode interface to implemented with @Override methods. 

So I'm wondering if I need to use an object other than XCompositeNode. For example, I can get this code to compile, although it throws a runtime error because the tree parameter is null. I'm substituting XCompositeNode with a XStackFrameNode in this attempt, instead:

XStackFrameNode stackFrameNode = new XStackFrameNode(null, currentStackFrame);
currentStackFrame.computeChildren(stackFrameNode);

 

 

I'm continuing to work on this issue. I'm not sure what to try next, and I may be missing the obvious. 

 

Thank you again for your patience.

 

Based on the explanation of my attempts to retrieve the values from the XStackFrame local variables container, do you have any pointers for what I should try next? 

 

Cheers!

0
Comment actions Permalink

Make your own implementation of XCompositeNode, something like this:

XCompositeNode node = new XCompositeNode() {
@Override
public void addChildren(@NotNull XValueChildrenList children, boolean last) {
// react on new added children - print or save somewhere
}

@Override
public void tooManyChildren(int remaining) {}

@Override
public void setAlreadySorted(boolean alreadySorted) {}

@Override
public void setErrorMessage(@NotNull String errorMessage) {}

@Override
public void setErrorMessage(@NotNull String errorMessage, @Nullable XDebuggerTreeNodeHyperlink link) {}

@Override
public void setMessage(@NotNull String message, @Nullable Icon icon, @NotNull SimpleTextAttributes attributes, @Nullable XDebuggerTreeNodeHyperlink link) {}
};
frame.computeChildren(node);
0
Comment actions Permalink

This is brilliant; it helped out a lot. I'm very grateful for your help. I was able to get past my roadblock. 

 

Would you mind helping with my next goal?

 

I am looking at how to get the name of the function in which the debugger is currently paused. I can see what I'm looking for within the local variables of the objects in my debugger. See image below... the name of the function is "greeting":

 

I'm guessing that I may have to obtain the method name asynchronously, just like getting variable values, described above.

 

what compute method... or other method can I use to get the name of the current method name? And will I be calling this compute method on the currentStackFrame object or a different object? Or am I overthinking this time because maybe I can just grab this method name synchronously? 

0
Comment actions Permalink

Public debugger API has only com.intellij.xdebugger.frame.XStackFrame#customizePresentation method, you provide a container and it will be populated with a position presentation (file name, line number, etc.)

Method name is java debugger specific, to be able to get it you need to cast your xframe to java frame (and so depend on java debugger impl). After that follow what you saw in the debug view: ((JavaStackFrame)xframe).getStackFrameProxy().location().method().name()

0
Comment actions Permalink

It looks like JavaStackFrame is special, in that I can't use the usual Alt + Enter to choose to import it. 

 

Is there an additional way to include this dependency into the file, for example, something to add to my gradle build task? Or do I need to add something to my plugin.xml file in order to be able to use the JavaStackFrame object? 

 

0
Comment actions Permalink

@Milligangregg try to add a dependency on Java plugin: `intellij { plugins = ['java'] }`, depedency on java plugin module should be also defined in plugin.xml

 

0
Comment actions Permalink

Alexander Zolotov I think this is what I need here:

com.intellij.debugger.engine.JavaStackFrame

This may be a straight forward and simple question, since I'm new to intellij plugin development; Is this an existing module that I would need to download first? If so, where is the best place to download from? I'm looking to see if I can find out now...

 

0
Comment actions Permalink

com.intellij.debugger.engine.JavaStackFrame

Yes, it's in Java plugin that you need to attach (as I described before)

No, you don't need to download anything additionally

0
Comment actions Permalink

Thank you, Alexander. This worked for me. 

 

In addition to adding this to the build.gradle file:

intellij {
version '2020.1.3'
plugins 'java' <--- added this
}

 

I also had to add this to the plugin.xml file:

<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.java</depends> <--- added this

 

First, I tried this: com.intellij.java which seemed to allow me to use JavaStackFrame, but then I invalidated cache and restarted intellij and it no longer worked. So I switched to using com.intellij.modules.java instead and it seemed to work, even after invalidating cache and restarting intellij. 

 

Cheers!

0
Comment actions Permalink

Thanks for detailed reply, I believe it might be useful for other users.

0

Please sign in to leave a comment.