Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions java/api.debugger.jpda/apichanges.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@
<!-- ACTUAL CHANGES BEGIN HERE: -->

<changes>
<change>
<api name="JPDADebuggerAPI"/>
<summary>Snapshot a value with Variable.asValue</summary>
<version major="3" minor="47"/>
<date day="28" month="10" year="2026"/>
<author login="jtulach"/>
<compatibility addition="yes"/>
<description>
New <code>asValue</code> method added to request
a <em>snapshot</em> of the current value of the variable.
</description>
<class name="Variable" package="org.netbeans.api.debugger.jpda"/>
</change>
<change>
<api name="JPDADebuggerAPI"/>
<summary>ObjectInfoProvider, JPDABreakpointEvent, JPDADebugger enhancements</summary>
Expand Down
2 changes: 1 addition & 1 deletion java/api.debugger.jpda/manifest.mf
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Manifest-Version: 1.0
OpenIDE-Module: org.netbeans.api.debugger.jpda/2
OpenIDE-Module-Localizing-Bundle: org/netbeans/api/debugger/jpda/Bundle.properties
OpenIDE-Module-Specification-Version: 3.46
OpenIDE-Module-Specification-Version: 3.47
OpenIDE-Module-Package-Dependencies: com.sun.jdi[VirtualMachineManager]

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@


/**
* Represents some variable in debugged JVM.
* Represents some variable in the debuggee.
*
* <pre style="background-color: rgb(255, 255, 102);">
* Since JDI interfaces evolve from one version to another, it's strongly recommended
Expand Down Expand Up @@ -56,10 +56,25 @@ public interface Variable {
/**
* Create an object in this JVM, which mirrors the value of this variable in
* the target JVM.
*
*
* @return The mirror object, or <code>null</code> when a mirror object can not be created.
* @since 2.44
*/
Object createMirrorObject();


/**
* Creates a <em>snapshot</em> of the variable value. It clones the variable
* object to assure it <em>sticks to its current debugee value</em>
* regardless if the variable state gets re-assigned in the future.
*
* This is an optional operation. Variables that have no notion of
* <em>snapshotting</em> may return {@code this} as the default
* implementation of this method does.
*
* @return a <em>snapshot</em> of the current variable value
* @since 3.47
*/
default Variable asValue() {
return this;
}
}
2 changes: 1 addition & 1 deletion java/debugger.jpda.ui/nbproject/project.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<compile-dependency/>
<run-dependency>
<release-version>2</release-version>
<specification-version>3.45</specification-version>
<specification-version>3.47</specification-version>
</run-dependency>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.api.debugger.ActionsManagerListener;
Expand All @@ -53,6 +51,7 @@
import org.netbeans.modules.debugger.jpda.JPDADebuggerImpl;
import org.netbeans.modules.debugger.jpda.ui.models.BreakpointsActionsProvider;
import org.netbeans.modules.debugger.jpda.ui.models.BreakpointsNodeModel;
import org.netbeans.modules.debugger.jpda.ui.views.BreakpointOutputWriter;
import org.netbeans.spi.debugger.ContextProvider;
import org.netbeans.spi.debugger.jpda.ObjectInfoProvider;
import org.netbeans.spi.viewmodel.NodeModel;
Expand All @@ -75,6 +74,7 @@ public class BreakpointOutput extends LazyActionsManagerListener
("\\\\");
private static final String threadNamePattern = "{threadName}";
private static final String classNamePattern = "{className}";
private static final String localsPattern = "{locals}";
private static final Pattern methodNamePattern = Pattern.compile
("\\{methodName\\}");
private static final Pattern lineNumberPattern = Pattern.compile
Expand Down Expand Up @@ -375,13 +375,18 @@ private void printOutEvent(DebuggerConsoleIO io, String format, JPDABreakpointEv
printText = selectCondition(printText, threadStartedCondition, false);
}
}

int printLocals = printText.indexOf(localsPattern);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor points about {locals}:

  • It is removed from wherever it appears, and the locals are always printed after the message.
  • Only the first occurrence is removed; a second {locals} is printed literally.

Both are fine if documented, but I couldn't find {locals} mentioned anywhere a user would see it, e.g. near the print text field in the breakpoint customizer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

locals are always printed after the message.

Right. That's the intention. I considered it, but dumping all the local variables (inherently multi line) in the middle of the (single line) message seem inappropriate.

a second {locals} is printed literally.

Should it be also removed? I guess we don't need to dump the variables twice, right?

mentioned anywhere a user would see it

Where do you want me to add a reference to {locals}? If there is some suitable place I happily add some text there. However I think the whole BreakpointOutput infrastructure {=expr}, {className}, etc. is mostly undocumented. Which my be the reason why not many people found they can configure log points themselves.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Printing the locals after the message makes sense to me. Yes, I'd remove every occurrence and print the locals once.

For documentation, the print text field in ActionsPanel (tfPrintText) has no tooltip. A tooltip listing the placeholders ({className}, {methodName}, {lineNumber}, {threadName}, {=expression}, {locals}) would be a small, natural place to put it, and it would cover the undocumented ones you mentioned too. It doesn't need to be part of this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tooltip expanded in 87121f3 now it looks like

tooltip

if (printLocals != -1) {
printText = printText.substring(0, printLocals) + printText.substring(printLocals + localsPattern.length());
}

// 5) resolve all expressions {=expression}
printExpressions(event, printText, (msg, action) -> {
io.printWithAction(msg, action, false);
}, (msg) -> {
io.println(msg, null);
});
printExpressions(event, printText, io::printWithAction);

if (printLocals != -1) {
printLocals(event, io::printWithAction);
}
}

/* Processing all expressions. Expressions take form of {=expression}.
Expand All @@ -390,8 +395,7 @@ private void printOutEvent(DebuggerConsoleIO io, String format, JPDABreakpointEv
final void printExpressions(
JPDABreakpointEvent event,
String message,
BiConsumer<String, Runnable> print,
Consumer<String> println
BreakpointOutputWriter io
) {
String printText = message;
Session session = event.getDebugger().getSession();
Expand All @@ -413,31 +417,22 @@ final void printExpressions(
}

String beforeExpression = printText.substring(0, m.start());
print.accept(beforeExpression, null);

CallStackFrame csf = null;
JPDAThread t = event.getThread();
if (t != null && !event.isThreadDeath()) {
try {
CallStackFrame[] topFramePtr = t.getCallStack(0, 1);
if (topFramePtr.length > 0) {
csf = topFramePtr[0];
}
} catch (AbsentInformationException aiex) {}
}
io.print(beforeExpression, null, false);

CallStackFrame csf = findTopFrame(event);
Variable refValue = theDebugger.evaluate(expression, csf);
if (refValue instanceof ObjectVariable obj && !isNull(obj)) {
var sessionRef = new WeakReference<>(session);
print.accept(obj.getValue(), () -> {
io.print(obj.getValue(), () -> {
var s = sessionRef.get();
var ok = s != null && ObjectInfoProvider.showObjectInfo(s, obj);
if (!ok) {
println.accept("Cannot display details of " + obj.getValue());
io.print("Cannot display details of " + obj.getValue(), null, true);
}
});
}, false);
} else {
String textualValue = refValue == null ? "null" : refValue.getValue();
print.accept(textualValue, null);
io.print(textualValue, null, false);
}
} catch (Exception e) {
// expression is invalid or cannot be evaluated
Expand All @@ -448,11 +443,10 @@ final void printExpressions(
BreakpointsActionsProvider.customize(bp);
};
dumpErrors.add(() -> {
print.accept(" ", null);
print.accept(msg, configure);
println.accept("");
io.print(" ", null, false);
io.print(msg, configure, true);
});
print.accept("{=" + expression + "}", configure);
io.print("{=" + expression + "}", configure, false);
}
// process the rest of the text
printText = printText.substring(m.end());
Expand All @@ -462,12 +456,53 @@ final void printExpressions(
printText = printText + "\n***\n"+ thr.getLocalizedMessage()+"\n***\n";
}
// remaining of the text with a new line
println.accept(printText);
io.print(printText, null, true);
for (var errors : dumpErrors) {
errors.run();
}
}

final void printLocals(JPDABreakpointEvent event, BreakpointOutputWriter io) {
CallStackFrame csf = findTopFrame(event);
if (csf != null) {
try {
for (var refValue : csf.getLocalVariables()) {
io.print(" ", null, false);
io.print(refValue.getName(), null, false);
io.print(" = ", null, false);
if (refValue instanceof ObjectVariable ov && !isNull(ov) && ov.asValue() instanceof ObjectVariable obj) {
var sessionRef = new WeakReference<>(event.getDebugger().getSession());
io.print(obj.getValue(), () -> {
var s = sessionRef.get();
var ok = s != null && ObjectInfoProvider.showObjectInfo(s, obj);
if (!ok) {
io.print("Cannot display details of " + obj.getValue(), null, true);
}
}, true);
} else {
io.print(refValue.getValue(), null, true);
}
}
} catch (AbsentInformationException ex) {
// no locals info
}
}
}


private CallStackFrame findTopFrame(JPDABreakpointEvent event) {
if (event.getThread() != null && !event.isThreadDeath()) {
try {
CallStackFrame[] topFramePtr = event.getThread().getCallStack(0, 1);
if (topFramePtr.length > 0) {
return topFramePtr[0];
}
} catch (AbsentInformationException aiex) {
}
}
return null;
}

private static boolean isNull(ObjectVariable obj) {
return obj.getUniqueID() == 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,12 @@
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.JEditorPane;
import org.netbeans.api.debugger.Breakpoint.VALIDITY;
import org.netbeans.api.debugger.DebuggerManager;
import org.netbeans.api.debugger.jpda.FieldBreakpoint;
import org.netbeans.api.debugger.jpda.JPDABreakpoint;
import org.netbeans.api.debugger.jpda.LineBreakpoint;
import org.netbeans.api.debugger.jpda.MethodBreakpoint;
import org.netbeans.api.debugger.jpda.CallStackFrame;
import org.netbeans.api.debugger.jpda.JPDADebugger;
import org.netbeans.api.debugger.jpda.JPDAThread;
import org.netbeans.spi.debugger.jpda.EditorContext;
import org.netbeans.spi.debugger.jpda.SourcePathProvider;
Expand All @@ -44,7 +37,6 @@
import org.openide.ErrorManager;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.Utilities;


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,18 +138,18 @@ private boolean isPrimitive (Object node) {
);


private Map fixedWatches = new LinkedHashMap();
private HashSet listeners;
private ContextProvider contextProvider;
private Map<KeyWrapper, String> fixedWatches = new LinkedHashMap<>();
private HashSet listeners;
private final ContextProvider contextProvider;


public FixedWatchesManager (ContextProvider contextProvider) {
this.contextProvider = contextProvider;
}

public void deleteAllFixedWatches() {
Collection nodes = new ArrayList(fixedWatches.keySet());
for (Iterator iter = nodes.iterator(); iter.hasNext();) {
Collection<KeyWrapper> nodes = new ArrayList<>(fixedWatches.keySet());
for (Iterator<KeyWrapper> iter = nodes.iterator(); iter.hasNext();) {
fixedWatches.remove(iter.next());
fireModelChanged(new ModelEvent.NodeChanged(FixedWatchesManager.this,
TreeModel.ROOT,
Expand Down Expand Up @@ -361,18 +361,9 @@ private void createFixedWatch (Object node) {
}
}

public void addFixedWatch (String name, Variable variable) {
public void addFixedWatch (String name, Variable orig) {
// Clone the variable to assure that it's unique and sticks to the JDI value.
if (variable instanceof Cloneable) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • previous obscure alternative of Variable.toFixed()
  • the AbstractVariable implemented Cloneable, but as Object.clone() method is protected...
  • reflection had to be used to call it
  • now it becomes simple toFixed() call

try { // terrible code to invoke the clone() method
java.lang.reflect.Method cloneMethod = variable.getClass().getMethod("clone", new Class[] {});
cloneMethod.setAccessible(true);
Object newVar = cloneMethod.invoke(variable, new Object[] {});
if (newVar instanceof Variable) {
variable = (Variable) newVar;
}
} catch (Exception ex) {} // Ignore any exceptions
}
Variable variable = orig.asValue();
fixedWatches.put (new KeyWrapper(variable), name);
fireModelChanged (new ModelEvent.NodeChanged(
this,
Expand Down
Loading
Loading