From 593e9b4a22b33621bd43cee107e521a1476a629f Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Fri, 18 Dec 2020 13:19:31 -0500 Subject: [PATCH] GP-511: Added "Pin" toggle to interpreters. --- Ghidra/Debug/Debugger/certification.manifest | 1 + .../src/main/help/help/TOC_Source.xml | 24 +++--- .../DebuggerInterpreterPlugin.html | 46 ++++++++++++ .../core/debug/gui/DebuggerResources.java | 22 +++++- ...tractDebuggerWrappedConsoleConnection.java | 64 ++++++++++++++-- .../DebuggerInterpreterConnection.java | 30 ++++++++ .../DebuggerInterpreterPlugin.java | 73 +++++++++++-------- .../DebuggerTraceManagerServicePlugin.java | 51 ++++++++----- .../services/DebuggerInterpreterService.java | 5 +- .../DebuggerInterpreterPluginTest.java | 61 ++++++---------- 10 files changed, 266 insertions(+), 111 deletions(-) create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterConnection.java diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index 65c19f4f55..241b0602fb 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -37,6 +37,7 @@ src/main/help/help/topics/DebuggerBreakpointsPlugin/images/breakpoint-mixed-ed.p src/main/help/help/topics/DebuggerBreakpointsPlugin/images/breakpoints-clear-all.png||GHIDRA||||END| src/main/help/help/topics/DebuggerBreakpointsPlugin/images/breakpoints-disable-all.png||GHIDRA||||END| src/main/help/help/topics/DebuggerBreakpointsPlugin/images/breakpoints-enable-all.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerListingPlugin/images/DebuggerGoToDialog.png||GHIDRA||||END| src/main/help/help/topics/DebuggerListingPlugin/images/DebuggerListingPlugin.png||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml index 833620d610..6e53ece5b8 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml @@ -78,28 +78,32 @@ sortgroup="d" target="help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html" /> - + + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html new file mode 100644 index 0000000000..daea6200f1 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html @@ -0,0 +1,46 @@ + + + + + + + Debugger: Interpreters + + + + + +

Debugger: Interpreters

+ +

For debuggers which have built-in interpreters (many do), and whose connectors expose that + interpreter in the model, the interpreters plugin can provide user access to it via a graphical + console emulator. The plugin leverages the existing interpreter console framework in Ghidra, so + the interface should be relatively familiar. Typically, the console is accessed via the Objects + window's Console action. + Output is displayed in a large text field, and user input is taken via a small text field at + the bottom. The prompt, commands, and outputs are all defined by the connector.

+ +

Actions

+ +

Each interpreter console has the following actions:

+ +

Clear Interpreter

+ +

This action is always available. It clears the console's ouput buffer.

+ +

Remove Interpreter

+ +

This action appears when the target's interpreter is not longer valid, i.e., the connection + was closed. It permanently closes the console. Note this action can only appear if this console + was pinned.

+ +

Pin Interpreter

+ +

This action is always available. Normally interpreter consoles are permanently closed + immediately upon the associated target interpreter becoming invalid, i.e., the connection was + closed. Pinning an interpreter keeps it open, but in a disabled state, so that the buffer can + be examined after invalidation.

+ + diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java index 608130991f..9b80f11f6f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java @@ -128,12 +128,12 @@ public interface DebuggerResources { ImageIcon ICON_SYNC = ResourceManager.loadImage("images/sync_enabled.png"); ImageIcon ICON_VISIBILITY = ResourceManager.loadImage("images/format-text-bold.png"); + ImageIcon ICON_PIN = ResourceManager.loadImage("images/pin.png"); // TODO: Find better icon? ImageIcon ICON_IMPORT = ResourceManager.loadImage("images/imported_bookmark.gif"); - ImageIcon ICON_BLANK = ResourceManager.loadImage("images/blank.png"); - ImageIcon ICON_PACKAGE = ResourceManager.loadImage("images/debugger32.png"); + HelpLocation HELP_PACKAGE = new HelpLocation("Debugger", "package"); String HELP_ANCHOR_PLUGIN = "plugin"; @@ -557,12 +557,28 @@ public interface DebuggerResources { String HELP_ANCHOR = "disconnect_all"; public static ActionBuilder builder(Plugin owner, Plugin helpOwner) { - return new ActionBuilder(owner.getName(), NAME).description(DESCRIPTION) + return new ActionBuilder(owner.getName(), NAME) + .description(DESCRIPTION) .menuIcon(ICON) .helpLocation(new HelpLocation(helpOwner.getName(), HELP_ANCHOR)); } } + interface PinInterpreterAction { + String NAME = "Pin Interpreter"; + String DESCRIPTION = "Prevent this Interpreter from closing automatically"; + Icon ICON = ICON_PIN; + String HELP_ANCHOR = "pin"; + + public static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(ownerName, NAME) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + abstract class AbstractRecordAction extends DockingAction { public static final String NAME = "Record"; public static final Icon ICON = ICON_TRACE; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java index d02122ec24..4709760510 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java @@ -23,9 +23,11 @@ import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.ImageIcon; +import docking.ActionContext; +import docking.action.ToggleDockingAction; import ghidra.app.plugin.core.console.CodeCompletion; import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.plugin.core.interpreter.InterpreterConnection; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.PinInterpreterAction; import ghidra.app.plugin.core.interpreter.InterpreterConsole; import ghidra.dbg.target.TargetConsole.Channel; import ghidra.dbg.target.TargetConsole.TargetConsoleListener; @@ -35,7 +37,7 @@ import ghidra.dbg.target.TargetObject; import ghidra.util.Msg; public abstract class AbstractDebuggerWrappedConsoleConnection - implements InterpreterConnection { + implements DebuggerInterpreterConnection { /** * We inherit console text output from interpreter listener, even though we may be listening to @@ -75,8 +77,13 @@ public abstract class AbstractDebuggerWrappedConsoleConnection getCompletions(String cmd) { // TODO: If GDB or WinDBG ever provides an API for completion.... + // TODO: Of course, that's another method on TargetInterpeter, too. return Collections.emptyList(); } public void setConsole(InterpreterConsole guiConsole) { + assert this.guiConsole == null; this.guiConsole = guiConsole; setErrWriter(guiConsole.getErrWriter()); setOutWriter(guiConsole.getOutWriter()); setStdIn(guiConsole.getStdin()); + + createActions(); + } + + protected void createActions() { + actionPin = PinInterpreterAction.builder(plugin) + .onAction(this::activatedPin) + .selected(pinned) + .build(); + guiConsole.addAction(actionPin); } public void setOutWriter(PrintWriter outWriter) { @@ -142,6 +171,10 @@ public abstract class AbstractDebuggerWrappedConsoleConnection consoles = new HashMap<>(); + protected final Map connections = new HashMap<>(); public DebuggerInterpreterPlugin(PluginTool tool) { super(tool); } @Override - public void showConsole(TargetConsole targetConsole) { - InterpreterConsole console; - synchronized (consoles) { - console = consoles.computeIfAbsent(targetConsole, c -> createConsole(targetConsole)); + public DebuggerInterpreterConnection showConsole(TargetConsole targetConsole) { + DebuggerInterpreterConnection conn; + synchronized (connections) { + conn = connections.computeIfAbsent(targetConsole, c -> createConnection(targetConsole)); } - console.show(); + conn.getInterpreterConsole().show(); + return conn; } @Override - public void showConsole(TargetInterpreter targetInterpreter) { - InterpreterConsole console; - synchronized (consoles) { - console = - consoles.computeIfAbsent(targetInterpreter, c -> createConsole(targetInterpreter)); + public DebuggerInterpreterConnection showConsole(TargetInterpreter targetInterpreter) { + DebuggerInterpreterConnection conn; + synchronized (connections) { + conn = connections.computeIfAbsent(targetInterpreter, + c -> createConnection(targetInterpreter)); } - console.show(); + conn.getInterpreterConsole().show(); + return conn; } protected void disableConsole(TargetObject targetConsole, InterpreterConsole guiConsole) { - InterpreterConsole old = consoles.get(targetConsole); - assert old == guiConsole; + DebuggerInterpreterConnection old; + synchronized (connections) { + old = connections.remove(targetConsole); + } + assert old.getInterpreterConsole() == guiConsole; SwingUtilities.invokeLater(() -> { if (guiConsole.isInputPermitted()) { guiConsole.setInputPermitted(false); guiConsole.setTransient(); guiConsole.setPrompt(">>INVALID<<"); - /** - * TODO: Should invisible ones just be removed? - * - * TODO: Would like setTransient to work like other providers, but - * InterpreterComponentProvider overrides it.... For now, I leave invisible disabled - * ones in the tool. User must show and click custom "remove interpreter" action. - */ - /*if (!console.isVisible()) { - console.dispose(); - }*/ } }); } - protected InterpreterConsole createConsole( - AbstractDebuggerWrappedConsoleConnection connection) { + protected void createConsole(AbstractDebuggerWrappedConsoleConnection connection) { InterpreterConsole console = consoleService.createInterpreterPanel(connection, true); connection.setConsole(console); connection.runInBackground(); - return console; } - protected InterpreterConsole createConsole(TargetConsole targetConsole) { - return createConsole(new DebuggerWrappedConsoleConnection(this, targetConsole)); + protected DebuggerInterpreterConnection createConnection(TargetConsole targetConsole) { + DebuggerWrappedConsoleConnection conn = + new DebuggerWrappedConsoleConnection(this, targetConsole); + createConsole(conn); + return conn; } - protected InterpreterConsole createConsole(TargetInterpreter targetInterpreter) { - return createConsole(new DebuggerWrappedInterpreterConnection(this, targetInterpreter)); + protected DebuggerInterpreterConnection createConnection( + TargetInterpreter targetInterpreter) { + DebuggerWrappedInterpreterConnection conn = + new DebuggerWrappedInterpreterConnection(this, targetInterpreter); + createConsole(conn); + return conn; + } + + public void destroyConsole(TargetObject targetConsole, InterpreterConsole guiConsole) { + DebuggerInterpreterConnection old; + synchronized (connections) { + old = connections.remove(targetConsole); + } + assert old.getInterpreterConsole() == guiConsole; + SwingUtilities.invokeLater(() -> { + guiConsole.dispose(); + }); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java index 3d7a5babe1..44783bc203 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java @@ -301,32 +301,37 @@ public class DebuggerTraceManagerServicePlugin extends Plugin @Override public void closeAllTraces() { - for (Trace trace : getOpenTraces()) { - closeTrace(trace); - } + Swing.runIfSwingOrRunLater(() -> { + for (Trace trace : getOpenTraces()) { + closeTrace(trace); + } + }); } @Override public void closeOtherTraces(Trace keep) { - for (Trace trace : getOpenTraces()) { - if (trace != keep) { - closeTrace(trace); + Swing.runIfSwingOrRunLater(() -> { + for (Trace trace : getOpenTraces()) { + if (trace != keep) { + closeTrace(trace); + } } - } - + }); } @Override public void closeDeadTraces() { - if (modelService == null) { - return; - } - for (Trace trace : getOpenTraces()) { - TraceRecorder recorder = modelService.getRecorder(trace); - if (recorder == null) { - closeTrace(trace); + Swing.runIfSwingOrRunLater(() -> { + if (modelService == null) { + return; } - } + for (Trace trace : getOpenTraces()) { + TraceRecorder recorder = modelService.getRecorder(trace); + if (recorder == null) { + closeTrace(trace); + } + } + }); } @AutoServiceConsumed @@ -837,10 +842,16 @@ public class DebuggerTraceManagerServicePlugin extends Plugin @Override public void closeTrace(Trace trace) { - if (trace.getConsumerList().contains(this)) { - firePluginEvent(new TraceClosedPluginEvent(getName(), trace)); - doTraceClosed(trace); - } + /** + * A provider may be reading the trace, likely via the Swing thread, so schedule this on the + * same thread to avoid a ClosedException. + */ + Swing.runIfSwingOrRunLater(() -> { + if (trace.getConsumerList().contains(this)) { + firePluginEvent(new TraceClosedPluginEvent(getName(), trace)); + doTraceClosed(trace); + } + }); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerInterpreterService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerInterpreterService.java index 807a6942d2..7f8df36f62 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerInterpreterService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerInterpreterService.java @@ -15,6 +15,7 @@ */ package ghidra.app.services; +import ghidra.app.plugin.core.debug.gui.interpreters.DebuggerInterpreterConnection; import ghidra.app.plugin.core.debug.gui.interpreters.DebuggerInterpreterPlugin; import ghidra.dbg.target.TargetConsole; import ghidra.dbg.target.TargetInterpreter; @@ -25,7 +26,7 @@ import ghidra.framework.plugintool.ServiceInfo; description = "Service for managing debugger interpreter panels" // ) public interface DebuggerInterpreterService { - void showConsole(TargetConsole console); + DebuggerInterpreterConnection showConsole(TargetConsole console); - void showConsole(TargetInterpreter interpreter); + DebuggerInterpreterConnection showConsole(TargetInterpreter interpreter); } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPluginTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPluginTest.java index 0ab1359f91..708bb2f338 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPluginTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPluginTest.java @@ -22,7 +22,8 @@ import java.awt.event.KeyEvent; import java.util.List; import java.util.Map; -import org.junit.*; +import org.junit.Before; +import org.junit.Test; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.interpreter.InterpreterComponentProvider; @@ -112,7 +113,7 @@ public class DebuggerInterpreterPluginTest extends AbstractGhidraHeadedDebuggerG } @Test - public void testInvalidateInterpreterDisablesConsole() throws Exception { + public void testInvalidateInterpreterDestroysConsole() throws Exception { createTestModel(); interpreterPlugin.showConsole(mb.testModel.session.interpreter); InterpreterComponentProvider interpreter = @@ -123,44 +124,24 @@ public class DebuggerInterpreterPluginTest extends AbstractGhidraHeadedDebuggerG ), Map.of(), "Invalidate interpreter"); waitForSwing(); + assertFalse(interpreter.isVisible()); + assertFalse(interpreter.isInTool()); + } + + @Test + public void testInvalidatePinnedInterpreterDisablesConsole() throws Exception { + createTestModel(); + DebuggerInterpreterConnection conn = + interpreterPlugin.showConsole(mb.testModel.session.interpreter); + InterpreterComponentProvider interpreter = + waitForComponentProvider(InterpreterComponentProvider.class); + conn.setPinned(true); + + mb.testModel.session.changeAttributes(List.of( + "Interpreter" // + ), Map.of(), "Invalidate interpreter"); + waitForSwing(); + assertFalse(interpreter.isInputPermitted()); } - - @Test - @Ignore("Haven't decided on proper behavior") - public void testInvalidateClosedDestroysConsole() throws Exception { - createTestModel(); - interpreterPlugin.showConsole(mb.testModel.session.interpreter); - InterpreterComponentProvider interpreter = - waitForComponentProvider(InterpreterComponentProvider.class); - - interpreter.setVisible(false); - waitForSwing(); - - mb.testModel.session.changeAttributes(List.of( - "Interpreter" // - ), Map.of(), "Invalidate interpreter"); - waitForSwing(); - - assertFalse(interpreter.isInTool()); - } - - @Test - @Ignore("Haven't decided on proper behavior") - public void testCloseInvalidatedDestroysConsole() throws Exception { - createTestModel(); - interpreterPlugin.showConsole(mb.testModel.session.interpreter); - InterpreterComponentProvider interpreter = - waitForComponentProvider(InterpreterComponentProvider.class); - - mb.testModel.session.changeAttributes(List.of( - "Interpreter" // - ), Map.of(), "Invalidate interpreter"); - waitForSwing(); - - interpreter.setVisible(false); - waitForSwing(); - - assertFalse(interpreter.isInTool()); - } }