GP-903: Moved "recovery data" and "upgrade" program prompts to debugger console.

This commit is contained in:
Dan 2021-05-14 16:08:04 -04:00
parent ca66feec30
commit 813dff60f0
9 changed files with 377 additions and 23 deletions

View File

@ -986,13 +986,29 @@ public interface DebuggerResources {
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName).description(DESCRIPTION)
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.menuGroup(GROUP)
.menuPath(NAME)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface OpenProgramAction {
String NAME = "Open Program";
Icon ICON = ICON_PROGRAM;
String DESCRIPTION = "Open the program";
String HELP_ANCHOR = "open_program";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
abstract class AbstractToggleBreakpointAction extends DockingAction {
public static final String NAME = "Toggle Breakpoint";
// TODO: A "toggle breakpoint" icon

View File

@ -29,6 +29,7 @@ import docking.ActionContext;
import docking.action.DockingActionIf;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.LogRow;
import ghidra.app.services.DebuggerConsoleService;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
@ -105,8 +106,13 @@ public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleServ
}
@Override
public void remove(ActionContext context) {
provider.remove(context);
public void removeFromLog(ActionContext context) {
provider.removeFromLog(context);
}
@Override
public boolean logContains(ActionContext context) {
return provider.logContains(context);
}
@Override
@ -127,4 +133,14 @@ public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleServ
public long getRowCount(Class<? extends ActionContext> ctxCls) {
return provider.getRowCount(ctxCls);
}
/**
* For testing: to verify the contents of a message delivered to the console log
*
* @param ctx the context
* @return the the log entry
*/
public LogRow getLogRow(ActionContext ctx) {
return provider.getLogRow(ctx);
}
}

View File

@ -109,9 +109,15 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
}
}
protected static class BoundAction {
protected final DockingActionIf action;
protected final ActionContext context;
/**
* An action bound to a context
*
* <p>
* This class is public for access by test cases only.
*/
public static class BoundAction {
public final DockingActionIf action;
public final ActionContext context;
public BoundAction(DockingActionIf action, ActionContext context) {
this.action = action;
@ -144,10 +150,22 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
}
}
protected static class ActionList extends ArrayList<BoundAction> {
/**
* A list of bound actions
*
* <p>
* This class is public for access by test cases only.
*/
public static class ActionList extends ArrayList<BoundAction> {
}
protected static class LogRow {
/**
* An entry in the console's log
*
* <p>
* This class is public for access by test cases only.
*/
public static class LogRow {
private final Icon icon;
private final String message;
private final Date date;
@ -419,13 +437,19 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
new Date(event.getTimeMillis()), context, computeToolbarActions(context)));
}
protected void remove(ActionContext context) {
protected void removeFromLog(ActionContext context) {
synchronized (buffer) {
LogRow r = logTableModel.deleteKey(context);
buffer.remove(r);
}
}
protected boolean logContains(ActionContext context) {
synchronized (buffer) {
return logTableModel.getMap().containsKey(context);
}
}
protected void addResolutionAction(DockingActionIf action) {
DockingActionIf replaced =
actionsByOwnerThenName.computeIfAbsent(action.getOwner(), o -> new LinkedHashMap<>())
@ -479,9 +503,17 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
}
protected long getRowCount(Class<? extends ActionContext> ctxCls) {
return logTableModel.getModelData()
.stream()
.filter(r -> ctxCls.isInstance(r.context))
.count();
synchronized (buffer) {
return logTableModel.getModelData()
.stream()
.filter(r -> ctxCls.isInstance(r.context))
.count();
}
}
public LogRow getLogRow(ActionContext ctx) {
synchronized (buffer) {
return logTableModel.getMap().get(ctx);
}
}
}

View File

@ -65,6 +65,7 @@ import utilities.util.SuppressableCallback.Suppression;
eventsConsumed = {
// ProgramSelectionPluginEvent.class, // TODO: Later or remove
// ProgramHighlightPluginEvent.class, // TODO: Later or remove
ProgramOpenedPluginEvent.class, // For auto-open log cleanup
ProgramClosedPluginEvent.class, // For marker set cleanup
ProgramLocationPluginEvent.class, // For static listing sync
TraceActivatedPluginEvent.class, // Trace/thread activation and register tracking
@ -275,6 +276,10 @@ public class DebuggerListingPlugin extends CodeBrowserPlugin implements Debugger
}
});
}
if (event instanceof ProgramOpenedPluginEvent) {
ProgramOpenedPluginEvent ev = (ProgramOpenedPluginEvent) event;
allProviders(p -> p.programOpened(ev.getProgram()));
}
if (event instanceof ProgramClosedPluginEvent) {
ProgramClosedPluginEvent ev = (ProgramClosedPluginEvent) event;
allProviders(p -> p.programClosed(ev.getProgram()));

View File

@ -85,6 +85,9 @@ import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.util.TraceAddressSpace;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.*;
import utilities.util.SuppressableCallback;
import utilities.util.SuppressableCallback.Suppression;
@ -352,6 +355,7 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi
protected FollowsCurrentThreadAction actionFollowsCurrentThread;
protected MultiStateDockingAction<AutoReadMemorySpec> actionAutoReadMemory;
protected DockingAction actionExportView;
protected DockingAction actionOpenProgram;
protected final DebuggerGoToDialog goToDialog;
@ -585,6 +589,15 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi
}
}
@AutoServiceConsumed
private void setConsoleService(DebuggerConsoleService consoleService) {
if (consoleService != null) {
if (actionOpenProgram != null) {
consoleService.addResolutionAction(actionOpenProgram);
}
}
}
protected void markTrackedStaticLocation(ProgramLocation location) {
Swing.runIfSwingOrRunLater(() -> {
if (location == null) {
@ -606,6 +619,17 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi
});
}
public void programOpened(Program program) {
if (!isMainListing()) {
return;
}
DomainFile df = program.getDomainFile();
DebuggerOpenProgramActionContext ctx = new DebuggerOpenProgramActionContext(df);
if (consoleService != null) {
consoleService.removeFromLog(ctx);
}
}
public void programClosed(Program program) {
if (program == markedProgram) {
removeOldStaticTrackingMarker();
@ -783,6 +807,11 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi
.onAction(this::activatedExportView)
.buildAndInstallLocal(this);
actionOpenProgram = OpenProgramAction.builder(plugin)
.withContext(DebuggerOpenProgramActionContext.class)
.onAction(this::activatedOpenProgram)
.build();
contextChanged();
}
@ -812,6 +841,11 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi
tool.showDialog(dialog);
}
private void activatedOpenProgram(DebuggerOpenProgramActionContext context) {
programManager.openProgram(context.getDomainFile(), DomainFile.DEFAULT_VERSION,
ProgramManager.OPEN_CURRENT);
}
protected void activatedLocationTracking(ActionContext ctx) {
doTrackSpec();
}
@ -934,7 +968,7 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi
}
protected void doSyncToStatic(ProgramLocation location) {
if (syncToStaticListing && location != null) {
if (isSyncToStaticListing() && location != null) {
ProgramLocation staticLoc = mappingService.getStaticLocationFromDynamic(location);
if (staticLoc != null) {
Swing.runIfSwingOrRunLater(() -> plugin.fireStaticLocationEvent(staticLoc));
@ -942,8 +976,58 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi
}
}
protected void doTryOpenProgram(DomainFile df, int version, int state) {
DebuggerOpenProgramActionContext ctx = new DebuggerOpenProgramActionContext(df);
if (consoleService != null && consoleService.logContains(ctx)) {
return;
}
if (df.canRecover()) {
if (consoleService != null) {
consoleService.log(DebuggerResources.ICON_MODULES, "<html>Program <b>" +
HTMLUtilities.escapeHTML(df.getPathname()) +
"</b> has recovery data. It must be opened manually.</html>", ctx);
}
return;
}
new TaskLauncher(new Task("Open " + df, true, false, false) {
@Override
public void run(TaskMonitor monitor) throws CancelledException {
Program program = null;
try {
program = (Program) df.getDomainObject(this, false, false, monitor);
programManager.openProgram(program, state);
}
catch (VersionException e) {
if (consoleService != null) {
consoleService.log(DebuggerResources.ICON_MODULES, "<html>Program <b>" +
HTMLUtilities.escapeHTML(df.getPathname()) +
"</b> was created with a different version of Ghidra." +
" It must be opened manually.</html>", ctx);
}
return;
}
catch (Exception e) {
if (consoleService != null) {
consoleService.log(DebuggerResources.ICON_LOG_ERROR, "<html>Program <b>" +
HTMLUtilities.escapeHTML(df.getPathname()) +
"</b> could not be opened: " + e + ". Try opening it manually.</html>",
ctx);
}
return;
}
finally {
if (program != null) {
program.release(this);
}
}
}
}, tool.getToolFrame());
}
protected void doCheckCurrentModuleMissing() {
if (importerService == null || consoleService == null) {
// Is there any reason to try to open the module if we're not syncing listings?
// I don't think so.
if (!isSyncToStaticListing()) {
return;
}
Trace trace = current.getTrace();
@ -967,9 +1051,7 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi
DomainFile df = ProgramURLUtils.getFileForHackedUpGhidraURL(tool.getProject(),
mapping.getStaticProgramURL());
if (df != null) {
// We're almost certainly preparing to goTo, so make it current
programManager.openProgram(df, DomainFile.DEFAULT_VERSION,
ProgramManager.OPEN_CURRENT);
doTryOpenProgram(df, DomainFile.DEFAULT_VERSION, ProgramManager.OPEN_CURRENT);
}
}
@ -995,14 +1077,19 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi
if (programManager != null && !toOpen.isEmpty()) {
for (DomainFile df : toOpen) {
// Do not presume a goTo is about to happen. There are no mappings, yet.
programManager.openProgram(df, DomainFile.DEFAULT_VERSION,
doTryOpenProgram(df, DomainFile.DEFAULT_VERSION,
ProgramManager.OPEN_VISIBLE);
}
}
if (importerService == null || consoleService == null) {
return;
}
for (TraceModule mod : missing) {
consoleService.log(DebuggerResources.ICON_LOG_ERROR,
"<html>The module <b><tt>" + HTMLUtilities.escapeHTML(mod.getName()) +
"</tt></b> was not found in the project",
"</tt></b> was not found in the project</html>",
new DebuggerMissingModuleActionContext(mod));
}
/**
@ -1144,6 +1231,11 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi
public void dispose() {
super.dispose();
removeOldListeners();
if (consoleService != null) {
if (actionOpenProgram != null) {
consoleService.removeResolutionAction(actionOpenProgram);
}
}
}
@Override

View File

@ -0,0 +1,55 @@
/* ###
* 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.listing;
import java.util.Objects;
import docking.ActionContext;
import ghidra.framework.model.DomainFile;
public class DebuggerOpenProgramActionContext extends ActionContext {
private final DomainFile df;
private final int hashCode;
public DebuggerOpenProgramActionContext(DomainFile df) {
this.df = df;
this.hashCode = Objects.hash(getClass(), df);
}
public DomainFile getDomainFile() {
return df;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DebuggerOpenProgramActionContext)) {
return false;
}
DebuggerOpenProgramActionContext that = (DebuggerOpenProgramActionContext) obj;
if (!this.df.equals(that.df)) {
return false;
}
return true;
}
}

View File

@ -867,12 +867,12 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
Msg.error(this, "Import service is not present");
}
importModuleFromFileSystem(context.getModule());
consoleService.remove(context); // TODO: Should remove when mapping is created
consoleService.removeFromLog(context); // TODO: Should remove when mapping is created
}
private void activatedMapMissingModule(DebuggerMissingModuleActionContext context) {
mapModuleTo(context.getModule());
consoleService.remove(context); // TODO: Should remove when mapping is created
consoleService.removeFromLog(context); // TODO: Should remove when mapping is created
}
private void toggledFilter(ActionContext ignored) {

View File

@ -51,7 +51,15 @@ public interface DebuggerConsoleService extends DebuggerConsoleLogger {
*
* @param context the context of the entry to remove
*/
void remove(ActionContext context);
void removeFromLog(ActionContext context);
/**
* Check if the console contains an actionable message for the given context
*
* @param context the context to check for
* @return true if present, false if absent
*/
boolean logContains(ActionContext context);
/**
* Add an action which might be applied to an actionable log message

View File

@ -19,6 +19,7 @@ import static ghidra.lifecycle.Unfinished.TODO;
import static org.junit.Assert.*;
import java.awt.*;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;
@ -37,10 +38,13 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.AbstractFollowsCurrentThreadAction;
import ghidra.app.plugin.core.debug.gui.action.*;
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin;
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.BoundAction;
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.LogRow;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerMissingModuleActionContext;
import ghidra.app.services.*;
import ghidra.app.util.viewer.listingpanel.ListingPanel;
import ghidra.async.SwingExecutorService;
import ghidra.framework.model.*;
import ghidra.plugin.importer.ImporterPlugin;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.Register;
@ -57,6 +61,9 @@ import ghidra.trace.model.modules.TraceModule;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.util.database.UndoableTransaction;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor;
public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
static LocationTrackingSpec getLocationTrackingSpec(String name) {
@ -1351,4 +1358,127 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI
assertEquals(tb.addr(0x00404321), listingProvider.getLocation().getAddress());
}
@Test
public void testSyncToStaticListingOpensModule() throws Exception {
DebuggerConsolePlugin consolePlugin = addPlugin(tool, DebuggerConsolePlugin.class);
createAndOpenTrace();
createAndOpenProgramFromTrace();
intoProject(tb.trace);
intoProject(program);
AddressSpace ss = program.getAddressFactory().getDefaultAddressSpace();
try (UndoableTransaction tid = UndoableTransaction.start(program, "Add block", true)) {
program.getMemory()
.createInitializedBlock(".text", ss.getAddress(0x00600000), 0x10000, (byte) 0,
monitor, false);
}
try (UndoableTransaction tid = tb.startTransaction()) {
DBTraceMemoryManager memory = tb.trace.getMemoryManager();
memory.addRegion("exe:.text", Range.atLeast(0L), tb.range(0x00400000, 0x0040ffff),
TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE);
TraceLocation from =
new DefaultTraceLocation(tb.trace, null, Range.atLeast(0L), tb.addr(0x00400000));
ProgramLocation to = new ProgramLocation(program, ss.getAddress(0x00600000));
mappingService.addMapping(from, to, 0x8000, false);
}
waitForProgram(program);
waitForDomainObject(tb.trace);
programManager.closeAllPrograms(true);
waitForPass(() -> assertEquals(0, programManager.getAllOpenPrograms().length));
traceManager.activateTrace(tb.trace);
waitForSwing();
listingProvider.getListingPanel()
.setCursorPosition(
new ProgramLocation(tb.trace.getProgramView(), tb.addr(0x00401234)),
EventTrigger.GUI_ACTION);
waitForSwing();
waitForPass(() -> assertEquals(1, programManager.getAllOpenPrograms().length));
assertTrue(java.util.List.of(programManager.getAllOpenPrograms()).contains(program));
assertFalse(consolePlugin
.logContains(new DebuggerOpenProgramActionContext(program.getDomainFile())));
}
@Test
public void testSyncToStaticLogsRecoverableProgram() throws Exception {
DebuggerConsolePlugin consolePlugin = addPlugin(tool, DebuggerConsolePlugin.class);
TestDummyDomainFolder root = new TestDummyDomainFolder(null, "root");
DomainFile df = new TestDummyDomainFile(root, "dummyFile") {
@Override
public boolean canRecover() {
return true;
}
};
listingProvider.doTryOpenProgram(df, DomainFile.DEFAULT_VERSION,
ProgramManager.OPEN_CURRENT);
waitForSwing();
DebuggerOpenProgramActionContext ctx = new DebuggerOpenProgramActionContext(df);
waitForPass(() -> assertTrue(consolePlugin.logContains(ctx)));
assertTrue(consolePlugin.getLogRow(ctx).getMessage().contains("recovery"));
}
@Test
public void testSyncToStaticLogsUpgradeableProgram() throws Exception {
DebuggerConsolePlugin consolePlugin = addPlugin(tool, DebuggerConsolePlugin.class);
TestDummyDomainFolder root = new TestDummyDomainFolder(null, "root");
DomainFile df = new TestDummyDomainFile(root, "dummyFile") {
@Override
public boolean canRecover() {
return false;
}
@Override
public DomainObject getDomainObject(Object consumer, boolean okToUpgrade,
boolean okToRecover, TaskMonitor monitor)
throws VersionException, IOException, CancelledException {
throw new VersionException();
}
};
listingProvider.doTryOpenProgram(df, DomainFile.DEFAULT_VERSION,
ProgramManager.OPEN_CURRENT);
waitForSwing();
DebuggerOpenProgramActionContext ctx = new DebuggerOpenProgramActionContext(df);
waitForPass(() -> assertTrue(consolePlugin.logContains(ctx)));
assertTrue(consolePlugin.getLogRow(ctx).getMessage().contains("version"));
}
@Test
public void testActionOpenProgram() throws Exception {
DebuggerConsolePlugin consolePlugin = addPlugin(tool, DebuggerConsolePlugin.class);
createProgram();
intoProject(program);
assertEquals(0, programManager.getAllOpenPrograms().length);
DebuggerOpenProgramActionContext ctx =
new DebuggerOpenProgramActionContext(program.getDomainFile());
consolePlugin.log(DebuggerResources.ICON_MODULES, "Test resolution", ctx);
waitForSwing();
LogRow row = consolePlugin.getLogRow(ctx);
assertEquals(1, row.getActions().size());
BoundAction boundAction = row.getActions().get(0);
assertEquals(listingProvider.actionOpenProgram, boundAction.action);
boundAction.perform();
waitForSwing();
waitForPass(() -> assertEquals(1, programManager.getAllOpenPrograms().length));
assertTrue(java.util.List.of(programManager.getAllOpenPrograms()).contains(program));
// TODO: Test this independent of this particular action?
assertNull(consolePlugin.getLogRow(ctx));
}
}