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());
- }
}