diff --git a/ide/api.debugger/src/org/netbeans/spi/debugger/ContextAwareSupport.java b/ide/api.debugger/src/org/netbeans/spi/debugger/ContextAwareSupport.java index 90f233e7d86f..2206da76630f 100644 --- a/ide/api.debugger/src/org/netbeans/spi/debugger/ContextAwareSupport.java +++ b/ide/api.debugger/src/org/netbeans/spi/debugger/ContextAwareSupport.java @@ -23,7 +23,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; -import java.util.ArrayList; import java.util.Collections; import java.util.Map; import java.util.logging.Level; diff --git a/ide/spi.debugger.ui/src/org/netbeans/spi/debugger/ui/PinWatchUISupport.java b/ide/spi.debugger.ui/src/org/netbeans/spi/debugger/ui/PinWatchUISupport.java index 86e08573d8d4..c6b114d8acff 100644 --- a/ide/spi.debugger.ui/src/org/netbeans/spi/debugger/ui/PinWatchUISupport.java +++ b/ide/spi.debugger.ui/src/org/netbeans/spi/debugger/ui/PinWatchUISupport.java @@ -18,9 +18,7 @@ */ package org.netbeans.spi.debugger.ui; -import java.beans.Customizer; import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -35,7 +33,6 @@ import org.netbeans.modules.debugger.ui.annotations.WatchAnnotationProvider; import org.netbeans.spi.debugger.DebuggerServiceRegistration; import org.openide.loaders.DataObjectNotFoundException; -import org.openide.util.Exceptions; import org.openide.util.NbBundle; /** diff --git a/java/api.debugger.jpda/apichanges.xml b/java/api.debugger.jpda/apichanges.xml index 8e66e2b7bda5..aa0dc431f1ed 100644 --- a/java/api.debugger.jpda/apichanges.xml +++ b/java/api.debugger.jpda/apichanges.xml @@ -21,7 +21,7 @@ --> - + + + + ObjectInfoProvider, JPDABreakpointEvent, JPDADebugger enhancements + + + + + + New service provider interface ObjectInfoProvider. + Added JPDABreakpointEvent.isThreadDeath() method. + Added new overload of evaluate method in JPDADebugger. + + + + + Randomly select listening port diff --git a/java/api.debugger.jpda/manifest.mf b/java/api.debugger.jpda/manifest.mf index 3bf025b79168..ddc41cd61606 100644 --- a/java/api.debugger.jpda/manifest.mf +++ b/java/api.debugger.jpda/manifest.mf @@ -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.44 +OpenIDE-Module-Specification-Version: 3.45 OpenIDE-Module-Package-Dependencies: com.sun.jdi[VirtualMachineManager] diff --git a/java/api.debugger.jpda/nbproject/org-netbeans-api-debugger-jpda.sig b/java/api.debugger.jpda/nbproject/org-netbeans-api-debugger-jpda.sig index cd4212c5f724..79afecbf50ed 100644 --- a/java/api.debugger.jpda/nbproject/org-netbeans-api-debugger-jpda.sig +++ b/java/api.debugger.jpda/nbproject/org-netbeans-api-debugger-jpda.sig @@ -80,7 +80,7 @@ CLSS public java.util.EventObject cons public init(java.lang.Object) fld protected java.lang.Object source intf java.io.Serializable -meth public java.lang.Object getSource() +meth public org.netbeans.api.debugger.jpda.JPDABreakpoint getSource() meth public java.lang.String toString() supr java.lang.Object diff --git a/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/JPDADebugger.java b/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/JPDADebugger.java index 7450c3120b5d..0208235f89eb 100644 --- a/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/JPDADebugger.java +++ b/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/JPDADebugger.java @@ -397,10 +397,26 @@ public List getAllThreads() { * @param expression a expression to be evaluated * * @return current value of given expression + * @throws org.netbeans.api.debugger.jpda.InvalidExpressionException */ public abstract Variable evaluate (String expression) throws InvalidExpressionException; + /** + * Evaluates given expression in the current context. + * + * @param expression a expression to be evaluated + * @param csf frame to evaluate the expression at + * + * @return current value of given expression + * @throws org.netbeans.api.debugger.jpda.InvalidExpressionException + * @since 3.45 + */ + public Variable evaluate (String expression, CallStackFrame csf) + throws InvalidExpressionException { + return evaluate(expression); + } + /** * Waits till the Virtual Machine is started and returns * {@link DebuggerStartException} if some problem occurres. diff --git a/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/event/JPDABreakpointEvent.java b/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/event/JPDABreakpointEvent.java index a53f471a1ffc..ad7f3c9f9074 100644 --- a/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/event/JPDABreakpointEvent.java +++ b/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/event/JPDABreakpointEvent.java @@ -21,6 +21,7 @@ import com.sun.jdi.ReferenceType; import com.sun.jdi.event.Event; +import com.sun.jdi.event.ThreadDeathEvent; import java.util.EventObject; import org.netbeans.api.debugger.jpda.*; @@ -105,6 +106,16 @@ public JPDABreakpointEvent ( this.referenceType = referenceType; this.variable = variable; } + + /** The breakpoint that generated this event. + * + * @return the breakpoint associated with this event + * @since 3.45 + */ + @Override + public JPDABreakpoint getSource() { + return (JPDABreakpoint) super.getSource(); + } /** * Returns result of condition evaluation. @@ -180,4 +191,13 @@ public void resume () { public boolean getResume () { return resume; } + + /** Checks if this event is representing {@link ThreadDeathEvent}. + * + * @return {@code true} or {@code false} + * @since 3.45 + */ + public boolean isThreadDeath() { + return event instanceof ThreadDeathEvent; + } } diff --git a/java/api.debugger.jpda/src/org/netbeans/spi/debugger/jpda/ObjectInfoProvider.java b/java/api.debugger.jpda/src/org/netbeans/spi/debugger/jpda/ObjectInfoProvider.java new file mode 100644 index 000000000000..4bd7f62d1b22 --- /dev/null +++ b/java/api.debugger.jpda/src/org/netbeans/spi/debugger/jpda/ObjectInfoProvider.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.spi.debugger.jpda; + +import org.netbeans.api.debugger.Session; +import org.netbeans.api.debugger.jpda.ObjectVariable; + +/** Provider of additional information about objects in the heap. + * Register your implementation as + *
+ * @DebuggerServiceRegistration(path="netbeans-JPDASession", types=ObjectInfoProvider.class)
+ * public final class YourProvider implements ObjectInfoProvider {
+ * ...
+ * }
+ * 
+ * @since 3.45 + */ +public interface ObjectInfoProvider { + /** Handles showing the info about provided object. + * + * @param session the debugging session sending the request + * @param obj the object to display additional information about + * @return {@code true} if the object information can be shown, + * {@code false} otherwise + */ + boolean display(Session session, ObjectVariable obj); + + + /** Requests additional information about an object to be shown. + * Iterates all providers registered in the + * {@code session.lookup(null, ObjectInfoProvider.class)}. + * + * @param session the debugging session that's requesting + * @param obj reference to an object in the heap + * @return {@code true} if the object information can be shown, + * {@code false} otherwise + */ + static boolean showObjectInfo(Session session, ObjectVariable obj) { + for (var output : session.lookup(null, ObjectInfoProvider.class)) { + if (output != null && output.display(session, obj)) { + return true; + } + } + return false; + } + +} diff --git a/java/debugger.jpda.ui/manifest.mf b/java/debugger.jpda.ui/manifest.mf index cbbc6aff8f2b..a860fc6b27b3 100644 --- a/java/debugger.jpda.ui/manifest.mf +++ b/java/debugger.jpda.ui/manifest.mf @@ -3,7 +3,7 @@ AutoUpdate-Show-In-Client: false OpenIDE-Module: org.netbeans.modules.debugger.jpda.ui/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/debugger/jpda/ui/Bundle.properties OpenIDE-Module-Layer: org/netbeans/modules/debugger/jpda/resources/mf-layer.xml -OpenIDE-Module-Specification-Version: 1.88 +OpenIDE-Module-Specification-Version: 1.89 OpenIDE-Module-Requires: org.netbeans.api.debugger.jpda.JPDADebuggerEngineImpl, org.netbeans.spi.debugger.ui OpenIDE-Module-Provides: org.netbeans.modules.debugger.jpda.ui diff --git a/java/debugger.jpda.ui/nbproject/project.xml b/java/debugger.jpda.ui/nbproject/project.xml index 63eccde39b9b..15bf0d2efcc3 100644 --- a/java/debugger.jpda.ui/nbproject/project.xml +++ b/java/debugger.jpda.ui/nbproject/project.xml @@ -40,7 +40,7 @@ 2 - 3.18 + 3.45 @@ -90,7 +90,7 @@ 2 - 1.81 + 1.144 diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java index 0cc56ae927d6..002797a361e7 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java @@ -22,22 +22,40 @@ import com.sun.jdi.AbsentInformationException; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.lang.reflect.InvocationTargetException; -import java.util.*; +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.*; +import org.netbeans.api.debugger.ActionsManagerListener; +import org.netbeans.api.debugger.Breakpoint; +import org.netbeans.api.debugger.DebuggerEngine; import org.netbeans.api.debugger.DebuggerManager; +import org.netbeans.api.debugger.DebuggerManagerListener; +import org.netbeans.api.debugger.LazyActionsManagerListener; import org.netbeans.api.debugger.Session; -import org.netbeans.api.debugger.jpda.*; +import org.netbeans.api.debugger.Watch; +import org.netbeans.api.debugger.jpda.CallStackFrame; +import org.netbeans.api.debugger.jpda.ExceptionBreakpoint; +import org.netbeans.api.debugger.jpda.InvalidExpressionException; +import org.netbeans.api.debugger.jpda.JPDABreakpoint; +import org.netbeans.api.debugger.jpda.JPDADebugger; +import org.netbeans.api.debugger.jpda.JPDAThread; +import org.netbeans.api.debugger.jpda.LineBreakpoint; +import org.netbeans.api.debugger.jpda.ObjectVariable; +import org.netbeans.api.debugger.jpda.ThreadBreakpoint; +import org.netbeans.api.debugger.jpda.Variable; import org.netbeans.api.debugger.jpda.event.JPDABreakpointEvent; import org.netbeans.api.debugger.jpda.event.JPDABreakpointListener; import org.netbeans.modules.debugger.jpda.DebuggerConsoleIO; 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.spi.debugger.ContextProvider; +import org.netbeans.spi.debugger.jpda.ObjectInfoProvider; import org.netbeans.spi.viewmodel.NodeModel; -import org.openide.util.Exceptions; import org.openide.util.NbBundle; /** @@ -83,8 +101,7 @@ public BreakpointOutput (ContextProvider contextProvider) { DebuggerManager.getDebuggerManager ().addDebuggerListener (DebuggerManager.PROP_BREAKPOINTS, this); } - - + // LazyActionsManagerListener .............................................. @Override @@ -141,13 +158,14 @@ public void substituteAndPrintText(String printText, JPDABreakpointEvent event) if (printText == null || printText.length () == 0) { return; } - printText = substitute(printText, event); JPDADebuggerImpl dbg; synchronized (lock) { dbg = (JPDADebuggerImpl) debugger; } + + if (dbg != null) { - dbg.getConsoleIO().println(printText, null); + printOutEvent(dbg.getConsoleIO(), printText, event); } } @@ -269,35 +287,21 @@ public void printValidityMessage(Breakpoint bp, Breakpoint.VALIDITY newValidity, // private methods ......................................................... /** - * threadName name of thread where breakpoint ocurres - * className name of class where breakpoint ocurres - * methodName name of method where breakpoint ocurres - * lineNumber number of line where breakpoint ocurres * - * @param printText - * @return + * @param format the format of the line to print */ - private String substitute (String printText, JPDABreakpointEvent event) { - + private void printOutEvent(DebuggerConsoleIO io, String format, JPDABreakpointEvent event) { + String printText = format; // 1) replace {threadName} by the name of current thread - JPDAThread t = event.getThread (); + JPDAThread t = event.getThread(); if (t != null) { printText = printText.replace(threadNamePattern, t.getName ()); } else { printText = printText.replace(threadNamePattern, "?"); } - boolean isThreadDeath = false; - if (t != null) { - try { - java.lang.reflect.Field f = event.getClass().getDeclaredField("event"); // NOI18N - f.setAccessible(true); - com.sun.jdi.event.Event je = (com.sun.jdi.event.Event) f.get(event); - isThreadDeath = (je instanceof com.sun.jdi.event.ThreadDeathEvent); - } catch (Exception ex) { - Exceptions.printStackTrace(ex); - } - } + boolean isThreadDeath = event.isThreadDeath(); + // 2) replace {className} by the name of current class if (event.getReferenceType () != null) { printText = printText.replace(classNamePattern, event.getReferenceType().name()); @@ -373,23 +377,47 @@ private String substitute (String printText, JPDABreakpointEvent event) { } // 5) resolve all expressions {=expression} + printExpressions(event, printText, (msg, action) -> { + io.printWithAction(msg, action, false); + }, (msg) -> { + io.println(msg, null); + }); + } + + /* Processing all expressions. Expressions take form of {=expression}. + * Invoked from {@code BreakpointOutputTest}. + */ + final void printExpressions( + JPDABreakpointEvent event, + String message, + BiConsumer print, + Consumer println + ) { + String printText = message; + Session session = event.getDebugger().getSession(); + var dumpErrors = new ArrayList(); for (;;) { Matcher m = expressionPattern.matcher (printText); if (!m.find ()) { break; } String expression = m.group (1); - String value = ""; try { JPDADebugger theDebugger; synchronized (lock) { if (debugger == null) { - return value; // The debugger is gone + // The debugger is gone + return; } theDebugger = debugger; } + + String beforeExpression = printText.substring(0, m.start()); + print.accept(beforeExpression, null); + CallStackFrame csf = null; - if (t != null && !isThreadDeath) { + JPDAThread t = event.getThread(); + if (t != null && !event.isThreadDeath()) { try { CallStackFrame[] topFramePtr = t.getCallStack(0, 1); if (topFramePtr.length > 0) { @@ -397,43 +425,51 @@ private String substitute (String printText, JPDABreakpointEvent event) { } } catch (AbsentInformationException aiex) {} } - try { - value = ((Variable) theDebugger.getClass().getMethod("evaluate", String.class, CallStackFrame.class). - invoke(theDebugger, expression, csf)).getValue(); - } catch (InvocationTargetException itex) { - if (itex.getTargetException() instanceof InvalidExpressionException) { - throw (InvalidExpressionException) itex.getTargetException(); - } - } catch (Exception ex) { - Exceptions.printStackTrace(ex); + Variable refValue = theDebugger.evaluate(expression, csf); + if (refValue instanceof ObjectVariable obj && !isNull(obj)) { + var sessionRef = new WeakReference<>(session); + print.accept(obj.getValue(), () -> { + var s = sessionRef.get(); + var ok = s != null && ObjectInfoProvider.showObjectInfo(s, obj); + if (!ok) { + println.accept("Cannot display details of " + obj.getValue()); + } + }); + } else { + String textualValue = refValue == null ? "null" : refValue.getValue(); + print.accept(textualValue, null); } - //value = theDebugger.evaluate (expression, csf).getValue (); - value = backslashEscapePattern.matcher (value). - replaceAll ("\\\\\\\\"); - value = dollarEscapePattern.matcher (value). - replaceAll ("\\\\\\$"); - } catch (InvalidExpressionException e) { + } catch (Exception e) { // expression is invalid or cannot be evaluated - String msg = e.getCause () != null ? - e.getCause ().getMessage () : e.getMessage (); - JPDADebuggerImpl dbg; - synchronized (lock) { - dbg = (JPDADebuggerImpl) debugger; - } - if (dbg != null) { - dbg.getConsoleIO().println ( - "Cannot evaluate expression '" + expression + "' : " + msg, - null - ); - } + Throwable cause = e.getCause(); + String msg = cause != null ? cause.getMessage() : e.getMessage(); + Breakpoint bp = event.getSource(); + Runnable configure = () -> { + BreakpointsActionsProvider.customize(bp); + }; + dumpErrors.add(() -> { + print.accept(" ", null); + print.accept(msg, configure); + println.accept(""); + }); + print.accept("{=" + expression + "}", configure); } - printText = m.replaceFirst (value); + // process the rest of the text + printText = printText.substring(m.end()); } Throwable thr = event.getConditionException(); if (thr != null) { printText = printText + "\n***\n"+ thr.getLocalizedMessage()+"\n***\n"; } - return printText; + // remaining of the text with a new line + println.accept(printText); + for (var errors : dumpErrors) { + errors.run(); + } + } + + private static boolean isNull(ObjectVariable obj) { + return obj.getUniqueID() == 0; } private static String selectCondition(String printText, String condition, boolean isTrue) { @@ -527,13 +563,11 @@ private void unhookBreakpoint (Breakpoint breakpoint) { private BreakpointsNodeModel breakpointsNodeModel; private BreakpointsNodeModel getBreakpointsNodeModel () { if (breakpointsNodeModel == null) { - List l = DebuggerManager.getDebuggerManager ().lookup - ("BreakpointsView", NodeModel.class); - Iterator it = l.iterator (); - while (it.hasNext ()) { - NodeModel nm = (NodeModel) it.next (); - if (nm instanceof BreakpointsNodeModel) { - breakpointsNodeModel = (BreakpointsNodeModel) nm; + List list = DebuggerManager.getDebuggerManager ().lookup + ("BreakpointsView", NodeModel.class); + for (NodeModel nodeModel : list) { + if (nodeModel instanceof BreakpointsNodeModel model) { + breakpointsNodeModel = model; break; } } diff --git a/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutputTest.java b/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutputTest.java new file mode 100644 index 000000000000..693fea79b38a --- /dev/null +++ b/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutputTest.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.debugger.jpda.ui; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import junit.framework.Test; +import org.netbeans.api.debugger.DebuggerManager; +import org.netbeans.api.debugger.Session; +import org.netbeans.api.debugger.jpda.ExceptionBreakpoint; +import org.netbeans.api.debugger.jpda.JPDADebugger; +import org.netbeans.api.debugger.jpda.JPDASupport; +import org.netbeans.api.debugger.jpda.event.JPDABreakpointEvent; +import org.netbeans.api.debugger.jpda.event.JPDABreakpointListener; +import org.netbeans.junit.NbTestCase; +import org.openide.util.Utilities; + +/** Testing {@link BreakpointOutput#printExpressions} + */ +public class BreakpointOutputTest extends NbTestCase { + private JPDASupport support; + private ExceptionBreakpoint breakPoint; + + + public BreakpointOutputTest(String name) { + super(name); + } + + public static Test suite() { + return JPDASupport.createTestSuite(BreakpointOutputTest.class); + } + + @Override + protected void setUp() throws Exception { + breakPoint = ExceptionBreakpoint.create( + "java.lang.IllegalStateException", + ExceptionBreakpoint.TYPE_EXCEPTION_CAUGHT_UNCAUGHT + ); + DebuggerManager.getDebuggerManager().addBreakpoint(breakPoint); + + final URI cpUrl = test.SampleMain.class.getProtectionDomain().getCodeSource().getLocation().toURI(); + System.setProperty ("test.dir.src", findSourcePath()); + support = JPDASupport.attach(new String[0], "test.SampleMain", new String[0], new File[] { + Utilities.toFile(cpUrl) + }); + } + + + + public void testBreakpointOutputLinks() throws Throwable { + final Session session = support.getDebugger().getSession(); + + // + // now the real test and collect the printed messages + // + + var collected = new StringWriter(); + var writer = new PrintWriter(collected); + var format = "breakpoint hit {=i}, {=when}, {=arr}, {=obj}"; + BreakpointOutput out = new BreakpointOutput(session); + assertOnBreakpoint((event) -> { + writer.append("on ").append(event.getThread().getName()).append("\n"); + try { + printExpressions(out, event, format, (msg, action) -> { + if (action != null) { + writer.append(""); + } + }, (msg) -> { + writer.append(msg).append("\n"); + }); + } catch (ReflectiveOperationException ex) { + ex.printStackTrace(writer); + } + }); + + // + // verify result + // + assertMatches(""" + on main + breakpoint hit 42, , , + """, collected.toString()); + } + + public void testArithmeticException() throws Exception { + // + // now the real test and collect the printed messages + // + + var collected = new StringWriter(); + var writer = new PrintWriter(collected); + var format = "arithmetic exception {=i/0}"; + BreakpointOutput out = new BreakpointOutput(support.getDebugger().getSession()); + assertOnBreakpoint((event) -> { + writer.append("on ").append(event.getThread().getName()).append("\n"); + try { + printExpressions(out, event, format, (msg, action) -> { + if (action != null) { + writer.append(""); + } + }, (msg) -> { + writer.append(msg).append("\n"); + }); + } catch (ReflectiveOperationException ex) { + ex.printStackTrace(writer); + } + }); + + // + // verify result + // + assertMatches(""" + on main + arithmetic exception + + """, collected.toString()); + + } + + private void assertOnBreakpoint(JPDABreakpointListener listener) { + int[] cnt = { 0 }; + JPDABreakpointListener countingListener = (event) -> { + cnt[0]++; + listener.breakpointReached(event); + }; + breakPoint.addJPDABreakpointListener(countingListener); + + // + // launch the debugger and run the test + // + + for (;;) { + support.waitState(JPDADebugger.STATE_STOPPED); + if (support.getDebugger().getState() == JPDADebugger.STATE_DISCONNECTED) { + break; + } + support.doContinue(); + } + + breakPoint.removeJPDABreakpointListener(countingListener); + + assertEquals("Breakpoint has been reached and listener is cleared", 1, cnt[0]); + } + + private String findSourcePath() throws IOException { + File curDir = getWorkDir(); + for (;;) { + var testSrc = new File(new File(new File(curDir, "test"), "unit"), "src"); + if (testSrc.isDirectory()) { + return testSrc.getPath(); + } + curDir = curDir.getParentFile(); + } + } + + /** + * Calls {@link BreakpointOutput#printExpressions}. + * Have to use reflection even the class and its tests are in the + * same package. They are loaded by different classloaders. + */ + private void printExpressions( + BreakpointOutput out, + JPDABreakpointEvent event, + String message, + BiConsumer print, + Consumer println + ) throws ReflectiveOperationException { + Method m = out.getClass().getDeclaredMethod( + "printExpressions", + JPDABreakpointEvent.class, String.class, BiConsumer.class, Consumer.class + ); + m.setAccessible(true); + m.invoke(out, event, message, print, println); + } + + private void assertMatches(String expected, String actual) { + if (!actual.matches(expected)) { + fail("Expected: " + expected + " but was: " + actual); + } + } +} diff --git a/java/debugger.jpda.ui/test/unit/src/test/SampleMain.java b/java/debugger.jpda.ui/test/unit/src/test/SampleMain.java new file mode 100644 index 000000000000..3d6992e8a764 --- /dev/null +++ b/java/debugger.jpda.ui/test/unit/src/test/SampleMain.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package test; + +import java.util.Arrays; + +public final class SampleMain { + private SampleMain() { + } + + public static void main(String... args) { + int i = 42; + String when = "Now"; + Object[] arr = new String[] { when }; + Object obj = SampleMain.class; + throw new IllegalStateException(when + Arrays.toString(arr) + obj + i); + } +} diff --git a/java/debugger.jpda/nbproject/project.properties b/java/debugger.jpda/nbproject/project.properties index 9c552b1b59fb..6fbc144acad1 100644 --- a/java/debugger.jpda/nbproject/project.properties +++ b/java/debugger.jpda/nbproject/project.properties @@ -23,7 +23,7 @@ javac.release=17 javadoc.arch=${basedir}/arch.xml jpda.classes.dir=${build.dir}/jpda/classes/ requires.nb.javac=true -spec.version.base=1.143.0 +spec.version.base=1.144.0 test-unit-sys-prop.test.dir.src=${basedir}/test/unit/src/ test-unit-sys-prop.netbeans.user=${basedir}/work/nb_user_dir test.unit.cp.extra=../java.source.nbjavac/build/test-nb-javac/cluster/modules/org-netbeans-modules-java-source-nbjavac-test.jar diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/DebuggerConsoleIO.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/DebuggerConsoleIO.java index 3e6191179b9f..f7ee86dbfe1c 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/DebuggerConsoleIO.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/DebuggerConsoleIO.java @@ -22,6 +22,7 @@ import java.lang.ref.Reference; import java.lang.ref.WeakReference; import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.debugger.jpda.JPDADebugger; import org.netbeans.api.io.InputOutput; import org.netbeans.modules.debugger.jpda.console.DebuggerOutput; @@ -53,7 +54,18 @@ public void println(String text, Line line) { public void println(String text, Line line, boolean important) { output.getIOManager().println(text, line, important); } - + + /** Prints a hyperlinked text and associates it with an action. + * + * @param text the text to print + * @param run action or {@code null} to invoke when the hyperlink is clicked + * @param newLine add a new line after the text + * @since 1.144 + */ + public void printWithAction(@NonNull String text, Runnable run, boolean newLine) { + output.getIOManager().print(text, run, newLine, false); + } + public static final class Line { private final String url; diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/console/DebuggerOutput.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/console/DebuggerOutput.java index 4e1950229f30..e81c692b9431 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/console/DebuggerOutput.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/console/DebuggerOutput.java @@ -31,12 +31,8 @@ import java.util.Iterator; import java.util.List; import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; import org.netbeans.api.debugger.ActionsManager; -import org.netbeans.api.debugger.ActionsManagerListener; import org.netbeans.api.debugger.DebuggerManager; -import org.netbeans.api.debugger.LazyActionsManagerListener; import org.netbeans.api.debugger.Session; import org.netbeans.api.debugger.jpda.AbstractDICookie; import org.netbeans.api.debugger.jpda.AttachingDICookie; diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/console/IOManager.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/console/IOManager.java index 8088ebaa4ff0..63eec3652af3 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/console/IOManager.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/console/IOManager.java @@ -111,6 +111,21 @@ public void println ( String text, Line line, boolean important + ) { + print(text, line == null ? null : new HyperlinkRunnable(line), true, important); + } + /** + * Prints given text to the output. + * @param text the text to print + * @param action an action or {@code null} to associte to the hyperlinked text + * @param newLine should a new line follow after the print + * @param important bring the output to the front? + */ + public void print ( + String text, + Runnable action, + boolean newLine, + boolean important ) { if (text == null) throw new NullPointerException (); @@ -124,7 +139,7 @@ public void println ( } } synchronized (buffer) { - buffer.addLast (new Text (text, line, important)); + buffer.addLast (new Text (text, action, newLine, important)); if (task == null) { task = new RequestProcessor("Debugger Output", 1).post (new Runnable () { public void run () { @@ -140,23 +155,16 @@ public void run () { int i, k = output.size (); for (i = 0; i < k; i++) { Text t = output.get(i); + var where = t.important ? debuggerErr : debuggerOut; + Hyperlink hl = t.action == null ? null : Hyperlink.from(t.action, t.important); + if (t.newLine) { + where.println(t.text, hl); + } else { + where.print(t.text, hl); + } + where.flush(); if (t.important) { - if (t.line != null) { - Hyperlink hl = Hyperlink.from(new HyperlinkRunnable(t.line), t.important); - debuggerErr.println(t.text, hl); - } else { - debuggerErr.println(t.text); - } debuggerIO.show(); - debuggerErr.flush(); - } else { - if (t.line != null) { - Hyperlink hl = Hyperlink.from(new HyperlinkRunnable(t.line), t.important); - debuggerOut.println(t.text, hl); - } else { - debuggerOut.println(t.text); - } - debuggerOut.flush(); } if (debugger != null) { debugger.actionStatusDisplayCallback(null, t.text); @@ -216,13 +224,15 @@ public void run() { } private static class Text { - private String text; - private Line line; - private boolean important; + final String text; + final Runnable action; + final boolean newLine; + final boolean important; - private Text (String text, Line line, boolean important) { + private Text (String text, Runnable action, boolean newLine, boolean important) { this.text = text; - this.line = line; + this.action = action; + this.newLine = newLine; this.important = important; } } diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/ThreadsCache.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/ThreadsCache.java index d684e90ed13d..a3243498685f 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/ThreadsCache.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/ThreadsCache.java @@ -22,7 +22,6 @@ import com.sun.jdi.ObjectCollectedException; import com.sun.jdi.ThreadGroupReference; import com.sun.jdi.ThreadReference; -import com.sun.jdi.VMDisconnectedException; import com.sun.jdi.VirtualMachine; import com.sun.jdi.event.Event; import com.sun.jdi.event.ThreadDeathEvent; diff --git a/platform/api.io/src/org/netbeans/api/io/OutputWriter.java b/platform/api.io/src/org/netbeans/api/io/OutputWriter.java index ac713fa4221d..02c9bde414c7 100644 --- a/platform/api.io/src/org/netbeans/api/io/OutputWriter.java +++ b/platform/api.io/src/org/netbeans/api/io/OutputWriter.java @@ -21,11 +21,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; -import java.util.Collections; -import java.util.EnumSet; import java.util.Locale; -import java.util.Set; -import org.netbeans.api.annotations.common.NullAllowed; import org.netbeans.spi.io.InputOutputProvider; /** diff --git a/profiler/debugger.jpda.heapwalk/nbproject/project.xml b/profiler/debugger.jpda.heapwalk/nbproject/project.xml index 44eacf5156d4..7ba812eec3ec 100644 --- a/profiler/debugger.jpda.heapwalk/nbproject/project.xml +++ b/profiler/debugger.jpda.heapwalk/nbproject/project.xml @@ -40,6 +40,7 @@ 2 + 3.45 @@ -51,12 +52,6 @@ 1.105 - - org.netbeans.modules.debugger.jpda.ui - - 1 - - org.netbeans.modules.profiler.heapwalker diff --git a/profiler/debugger.jpda.heapwalk/src/org/netbeans/modules/debugger/jpda/heapwalk/models/HeapActionsFilter.java b/profiler/debugger.jpda.heapwalk/src/org/netbeans/modules/debugger/jpda/heapwalk/models/HeapActionsFilter.java index 451f3279aaca..2c05e148b7ca 100644 --- a/profiler/debugger.jpda.heapwalk/src/org/netbeans/modules/debugger/jpda/heapwalk/models/HeapActionsFilter.java +++ b/profiler/debugger.jpda.heapwalk/src/org/netbeans/modules/debugger/jpda/heapwalk/models/HeapActionsFilter.java @@ -69,16 +69,19 @@ }) public class HeapActionsFilter implements NodeActionsProviderFilter { - private JPDADebugger debugger; - private RequestProcessor rp; + private final JPDADebugger debugger; + private final RequestProcessor rp; + private final ShowObjectVariableInHeap show; /** Creates a new instance of HeapActionsFilter */ public HeapActionsFilter(ContextProvider contextProvider) { debugger = contextProvider.lookupFirst(null, JPDADebugger.class); - rp = contextProvider.lookupFirst(null, RequestProcessor.class); - if (rp == null) { - rp = new RequestProcessor(HeapActionsFilter.class); + RequestProcessor tmp = contextProvider.lookupFirst(null, RequestProcessor.class); + if (tmp == null) { + tmp = new RequestProcessor(HeapActionsFilter.class); } + this.rp = tmp; + this.show = new ShowObjectVariableInHeap(debugger, rp); } @Override @@ -126,55 +129,7 @@ public boolean isEnabled (Object node) { public void perform (Object[] nodes) { ObjectVariable var = (ObjectVariable) nodes[0]; if (var.getUniqueID() == 0L) return ; - final InstancesView instances = openInstances(true); - final Reference varRef = new WeakReference(var); - final Reference debuggerRef = new WeakReference(debugger); - InstancesView.HeapFragmentWalkerProvider provider = - new InstancesView.HeapFragmentWalkerProvider() { - @Override - public synchronized HeapFragmentWalker getHeapFragmentWalker() { - HeapFragmentWalker hfw = instances.getCurrentFragmentWalker(); - HeapImpl heap = (hfw != null) ? (HeapImpl) hfw.getHeapFragment() : null; - JPDADebugger debugger = debuggerRef.get(); - if (heap == null || debugger != null && heap.getDebugger() != debugger) { - heap = new HeapImpl(debugger); - hfw = new DebuggerHeapFragmentWalker(heap); - } - final ObjectVariable var = varRef.get(); - final HeapFragmentWalker fhfw = hfw; - if (var != null) { - final HeapImpl fheap = heap; - rp.post(new Runnable() { - @Override - public void run() { - final Instance instance = InstanceImpl.createInstance(fheap, var); - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - fhfw.getInstancesController().showInstance(instance); - } - }); - } - }); - //Instance instance = InstanceImpl.createInstance(heap, var); - //hfw.getInstancesController().showInstance(instance); - } - return hfw; - } - }; - instances.setHeapFragmentWalkerProvider(provider); - } - - private InstancesView openInstances (boolean activate) { - TopComponent view = WindowManager.getDefault().findTopComponent("dbgInstances"); - if (view == null) { - throw new IllegalArgumentException("dbgInstances"); - } - view.open(); - if (activate) { - view.requestActive(); - } - return (InstancesView) view; + show.showObjectVariable(var); } }, diff --git a/profiler/debugger.jpda.heapwalk/src/org/netbeans/modules/debugger/jpda/heapwalk/models/ShowObjectVariableDetails.java b/profiler/debugger.jpda.heapwalk/src/org/netbeans/modules/debugger/jpda/heapwalk/models/ShowObjectVariableDetails.java new file mode 100644 index 000000000000..a1555f4b5a45 --- /dev/null +++ b/profiler/debugger.jpda.heapwalk/src/org/netbeans/modules/debugger/jpda/heapwalk/models/ShowObjectVariableDetails.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.debugger.jpda.heapwalk.models; + +import org.netbeans.api.debugger.Session; +import org.netbeans.api.debugger.jpda.JPDADebugger; +import org.netbeans.api.debugger.jpda.ObjectVariable; +import org.netbeans.spi.debugger.ContextProvider; +import org.netbeans.spi.debugger.DebuggerServiceRegistration; +import org.netbeans.spi.debugger.jpda.ObjectInfoProvider; +import org.openide.util.RequestProcessor; + +@DebuggerServiceRegistration(path="netbeans-JPDASession", types=ObjectInfoProvider.class) +public final class ShowObjectVariableDetails implements ObjectInfoProvider { + private static final RequestProcessor RP = new RequestProcessor("Variable details"); + private final JPDADebugger debugger; + + public ShowObjectVariableDetails(ContextProvider provider) { + debugger = provider.lookupFirst(null, JPDADebugger.class); + assert debugger != null; + } + + @Override + public boolean display(Session session, ObjectVariable v) { + ShowObjectVariableInHeap show = new ShowObjectVariableInHeap(debugger, RP); + show.showObjectVariable(v); + return true; + } +} diff --git a/profiler/debugger.jpda.heapwalk/src/org/netbeans/modules/debugger/jpda/heapwalk/models/ShowObjectVariableInHeap.java b/profiler/debugger.jpda.heapwalk/src/org/netbeans/modules/debugger/jpda/heapwalk/models/ShowObjectVariableInHeap.java new file mode 100644 index 000000000000..24b5f71f0ab7 --- /dev/null +++ b/profiler/debugger.jpda.heapwalk/src/org/netbeans/modules/debugger/jpda/heapwalk/models/ShowObjectVariableInHeap.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.debugger.jpda.heapwalk.models; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import javax.swing.SwingUtilities; +import org.netbeans.api.debugger.jpda.JPDADebugger; +import org.netbeans.api.debugger.jpda.ObjectVariable; +import org.netbeans.lib.profiler.heap.Instance; +import org.netbeans.modules.debugger.jpda.heapwalk.HeapImpl; +import org.netbeans.modules.debugger.jpda.heapwalk.InstanceImpl; +import org.netbeans.modules.debugger.jpda.heapwalk.views.DebuggerHeapFragmentWalker; +import org.netbeans.modules.debugger.jpda.heapwalk.views.InstancesView; +import org.netbeans.modules.profiler.heapwalk.HeapFragmentWalker; +import org.openide.util.RequestProcessor; +import org.openide.windows.TopComponent; +import org.openide.windows.WindowManager; + +final class ShowObjectVariableInHeap { + private final Reference debuggerRef; + private final RequestProcessor rp; + + ShowObjectVariableInHeap(JPDADebugger debugger, RequestProcessor rp) { + this.debuggerRef = new WeakReference<>(debugger); + this.rp = rp; + } + + void showObjectVariable(ObjectVariable var) { + final InstancesView instances = openInstances(true); + final Reference varRef = new WeakReference<>(var); + InstancesView.HeapFragmentWalkerProvider provider + = new InstancesView.HeapFragmentWalkerProvider() { + @Override + public synchronized HeapFragmentWalker getHeapFragmentWalker() { + HeapFragmentWalker hfw = instances.getCurrentFragmentWalker(); + HeapImpl heap = (hfw != null) ? (HeapImpl) hfw.getHeapFragment() : null; + JPDADebugger debugger = debuggerRef.get(); + if (heap == null || debugger != null && heap.getDebugger() != debugger) { + heap = new HeapImpl(debugger); + hfw = new DebuggerHeapFragmentWalker(heap); + } + final ObjectVariable var = varRef.get(); + final HeapFragmentWalker fhfw = hfw; + if (var != null) { + final HeapImpl fheap = heap; + rp.post(new Runnable() { + @Override + public void run() { + final Instance instance = InstanceImpl.createInstance(fheap, var); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + fhfw.getInstancesController().showInstance(instance); + } + }); + } + }); + //Instance instance = InstanceImpl.createInstance(heap, var); + //hfw.getInstancesController().showInstance(instance); + } + return hfw; + } + }; + instances.setHeapFragmentWalkerProvider(provider); + } + + private InstancesView openInstances(boolean activate) { + TopComponent view = WindowManager.getDefault().findTopComponent("dbgInstances"); + if (view == null) { + throw new IllegalArgumentException("dbgInstances"); + } + view.open(); + if (activate) { + view.requestActive(); + } + return (InstancesView) view; + } + +}