GP-511: Added "Pin" toggle to interpreters.

This commit is contained in:
Dan 2020-12-18 13:19:31 -05:00
parent 5ea7996b7d
commit 593e9b4a22
10 changed files with 266 additions and 111 deletions

View File

@ -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|

View File

@ -78,28 +78,32 @@
sortgroup="d"
target="help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html" />
<tocdef id="DebuggerThreadsPlugin" text="Threads and Traces"
<tocdef id="DebuggerInterpreterPlugin" text="Interpreters"
sortgroup="e"
target="help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html" />
<tocdef id="DebuggerThreadsPlugin" text="Threads and Traces"
sortgroup="f"
target="help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html" />
<tocdef id="DebuggerTraceManagerServicePlugin" text="Trace Management"
sortgroup="f"
sortgroup="g"
target="help/topics/DebuggerTraceManagerServicePlugin/DebuggerTraceManagerServicePlugin.html" />
<tocdef id="DebuggerRegistersPlugin" text="Registers"
sortgroup="g"
sortgroup="h"
target="help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html" />
<tocdef id="DebuggerListingPlugin" text="Dynamic Listing"
sortgroup="h"
sortgroup="i"
target="help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html" />
<tocdef id="DebuggerStackPlugin" text="Stack"
sortgroup="i"
sortgroup="j"
target="help/topics/DebuggerStackPlugin/DebuggerStackPlugin.html" />
<tocdef id="DebuggerBreakpointsPlugin" text="Breakpoints"
sortgroup="j"
sortgroup="k"
target="help/topics/DebuggerBreakpointsPlugin/DebuggerBreakpointsPlugin.html" >
<tocdef id="DebuggerBreakpointMarkerPlugin" text="In the Listings"
@ -108,15 +112,15 @@
</tocdef>
<tocdef id="DebuggerRegionsPlugin" text="Memory Regions"
sortgroup="k"
sortgroup="l"
target="help/topics/DebuggerRegionsPlugin/DebuggerRegionsPlugin.html" />
<tocdef id="DebuggerTimePlugin" text="Time"
sortgroup="l"
sortgroup="m"
target="help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html" />
<tocdef id="DebuggerModulesPlugin" text="Modules and Sections"
sortgroup="m"
sortgroup="n"
target="help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html" >
<tocdef id="DebuggerStaticMappingPlugin" text="Static Mappings"
@ -125,7 +129,7 @@
</tocdef>
<tocdef id="DebuggerBots" text="Bots: Workflow Automation"
sortgroup="n"
sortgroup="o"
target="help/topics/DebuggerBots/DebuggerBots.html" />
</tocdef>
</tocref>

View File

@ -0,0 +1,46 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<TITLE>Debugger: Interpreters</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="../../shared/Frontpage.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="plugin"></A><A name="interpreter"></A>Debugger: Interpreters</H1>
<P>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 <A href=
"help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html#console">Console</A> 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.</P>
<H2>Actions</H2>
<P>Each interpreter console has the following actions:</P>
<H3><A name="Clear_Interpreter"></A>Clear Interpreter</H3>
<P>This action is always available. It clears the console's ouput buffer.</P>
<H3><A name="Remove_Interpreter"></A>Remove Interpreter</H3>
<P>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.</P>
<H3><A name="pin"></A>Pin Interpreter</H3>
<P>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.</P>
</BODY>
</HTML>

View File

@ -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;

View File

@ -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<T extends TargetObject>
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<T extends TargetO
@Override
public void invalidated(TargetObject object, String reason) {
if (object == targetConsole) { // Redundant
running.set(false);
plugin.disableConsole(targetConsole, guiConsole);
if (pinned) {
running.set(false);
plugin.disableConsole(targetConsole, guiConsole);
}
else {
plugin.destroyConsole(targetConsole, guiConsole);
}
}
}
}
@ -92,6 +99,12 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
protected PrintWriter outWriter;
protected PrintWriter errWriter;
protected ToggleDockingAction actionPin;
protected boolean pinned = false;
// TODO: Fix InterpreterPanelService to take plugin name instead of just using title
protected boolean firstTimeAskedTitle = true;
public AbstractDebuggerWrappedConsoleConnection(DebuggerInterpreterPlugin plugin,
T targetConsole) {
this.plugin = plugin;
@ -103,7 +116,11 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
@Override
public String getTitle() {
return "Interpreter: " + targetConsole.getDisplay();
if (firstTimeAskedTitle) {
firstTimeAskedTitle = false;
return plugin.getName();
}
return targetConsole.getDisplay();
}
@Override
@ -114,14 +131,26 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
@Override
public List<CodeCompletion> 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<T extends TargetO
thread.start();
}
private void activatedPin(ActionContext ignore) {
pinned = actionPin.isSelected();
}
protected void run() {
try {
while (running.get()) {
@ -164,4 +197,25 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
Msg.debug(this, "Lost console?");
}
}
@Override
public InterpreterConsole getInterpreterConsole() {
return guiConsole;
}
@Override
public TargetObject getTargetConsole() {
return targetConsole;
}
@Override
public boolean isPinned() {
return pinned;
}
@Override
public void setPinned(boolean pinned) {
this.pinned = pinned;
actionPin.setSelected(pinned);
}
}

View File

@ -0,0 +1,30 @@
/* ###
* IP: GHIDRA
*
* Licensed 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 ghidra.app.plugin.core.debug.gui.interpreters;
import ghidra.app.plugin.core.interpreter.InterpreterConnection;
import ghidra.app.plugin.core.interpreter.InterpreterConsole;
import ghidra.dbg.target.TargetObject;
public interface DebuggerInterpreterConnection extends InterpreterConnection {
void setPinned(boolean pinned);
boolean isPinned();
TargetObject getTargetConsole();
InterpreterConsole getInterpreterConsole();
}

View File

@ -51,66 +51,77 @@ public class DebuggerInterpreterPlugin extends AbstractDebuggerPlugin
@AutoServiceConsumed
protected InterpreterPanelService consoleService;
protected final Map<TargetObject, InterpreterConsole> consoles = new HashMap<>();
protected final Map<TargetObject, DebuggerInterpreterConnection> 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();
});
}
}

View File

@ -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

View File

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

View File

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