GP-1808: Added 'Run to Address'-type actions to right-click menu for some connectors.

This commit is contained in:
Dan 2023-02-07 12:23:16 -05:00
parent 44d7c4f031
commit bde529b4d5
39 changed files with 1663 additions and 136 deletions

View file

@ -612,9 +612,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
public DebuggerControlPlugin(PluginTool tool) {
super(tool);
tool.addContextListener(this);
createActions();
}

View file

@ -0,0 +1,199 @@
/* ###
* 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.control;
import java.util.*;
import docking.ActionContext;
import docking.Tool;
import docking.action.*;
import docking.actions.PopupActionProvider;
import ghidra.app.context.ProgramLocationActionContext;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.services.*;
import ghidra.dbg.target.TargetMethod;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.util.PathPredicates;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.address.Address;
import ghidra.program.util.MarkerLocation;
import ghidra.program.util.ProgramLocation;
import ghidra.trace.model.program.TraceProgramView;
import ghidra.trace.model.target.TraceObject;
import ghidra.util.Msg;
@PluginInfo(
shortDescription = "Debugger model method actions",
description = "Adds context actions to the GUI, generically, based on the model's methods",
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.RELEASED,
eventsConsumed = {
},
servicesRequired = {
DebuggerStaticMappingService.class,
})
public class DebuggerMethodActionsPlugin extends Plugin implements PopupActionProvider {
public static final String GROUP_METHODS = "Debugger Methods";
private static String getDisplay(TargetMethod method) {
String display = method.getDisplay();
if (display != null) {
return display;
}
return method.getName();
}
class InvokeMethodAction extends DockingAction {
private final TargetMethod method;
public InvokeMethodAction(TargetMethod method) {
super(getDisplay(method), DebuggerMethodActionsPlugin.this.getName());
this.method = method;
setPopupMenuData(new MenuData(new String[] { getName() }, GROUP_METHODS));
}
@Override
public void actionPerformed(ActionContext context) {
Map<String, Object> arguments = collectArguments(method.getParameters(), context);
if (arguments == null) {
// Context changed out from under me?
return;
}
method.invoke(arguments).thenAccept(result -> {
if (consoleService != null && method.getReturnType() != Void.class) {
consoleService.log(null, getDisplay(method) + " returned " + result);
}
}).exceptionally(ex -> {
tool.setStatusInfo(
"Invocation of " + getDisplay(method) + " failed: " + ex.getMessage(), true);
Msg.error(this, "Invocation of " + method.getPath() + " failed", ex);
return null;
});
}
}
@AutoServiceConsumed
private DebuggerTraceManagerService traceManager;
@AutoServiceConsumed
private DebuggerStaticMappingService mappingService;
@AutoServiceConsumed
private DebuggerConsoleService consoleService;
@SuppressWarnings("unused")
private final AutoService.Wiring autoServiceWiring;
public DebuggerMethodActionsPlugin(PluginTool tool) {
super(tool);
autoServiceWiring = AutoService.wireServicesProvidedAndConsumed(this);
tool.addPopupActionProvider(this);
}
@Override
public List<DockingActionIf> getPopupActions(Tool tool, ActionContext context) {
TargetObject curObj = getCurrentTargetObject();
if (curObj == null) {
return List.of();
}
List<DockingActionIf> result = new ArrayList<>();
PathPredicates matcher = curObj.getModel()
.getRootSchema()
.matcherForSuitable(TargetMethod.class, curObj.getPath());
for (TargetObject obj : matcher.getCachedSuccessors(curObj.getModel().getModelRoot())
.values()) {
if (!(obj instanceof TargetMethod method)) {
continue;
}
Map<String, Object> arguments = collectArguments(method.getParameters(), context);
if (arguments == null) {
continue;
}
result.add(new InvokeMethodAction(method));
}
return result;
}
private TargetObject getCurrentTargetObject() {
if (traceManager == null) {
return null;
}
DebuggerCoordinates current = traceManager.getCurrent();
TraceRecorder recorder = current.getRecorder();
if (recorder == null) {
return null;
}
TraceObject object = current.getObject();
if (object != null) {
return recorder.getTargetObject(object);
}
return recorder.getFocus();
}
private Address dynamicAddress(ProgramLocation loc) {
if (loc.getProgram() instanceof TraceProgramView) {
return loc.getAddress();
}
if (traceManager == null) {
return null;
}
ProgramLocation dloc =
mappingService.getDynamicLocationFromStatic(traceManager.getCurrentView(), loc);
if (dloc == null) {
return null;
}
return dloc.getByteAddress();
}
private Map<String, Object> collectArguments(TargetParameterMap params, ActionContext context) {
// The only required non-defaulted argument allowed must be an Address
// There must be an Address parameter
ParameterDescription<?> addrParam = null;
for (ParameterDescription<?> p : params.values()) {
if (p.type == Address.class) {
if (addrParam != null) {
return null;
}
addrParam = p;
}
else if (p.required && p.defaultValue == null) {
return null;
}
}
if (addrParam == null) {
return null;
}
if (context instanceof ProgramLocationActionContext ctx) {
Address address = dynamicAddress(ctx.getLocation());
if (address == null) {
return null;
}
return Map.of(addrParam.name, address);
}
if (context.getContextObject() instanceof MarkerLocation ml) {
Address address = dynamicAddress(new ProgramLocation(ml.getProgram(), ml.getAddr()));
if (address == null) {
return null;
}
return Map.of(addrParam.name, address);
}
return null;
}
}

View file

@ -0,0 +1,279 @@
/* ###
* 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.control;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import org.jdom.JDOMException;
import org.junit.Before;
import org.junit.Test;
import docking.action.DockingActionIf;
import generic.Unique;
import ghidra.app.context.ProgramLocationActionContext;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin;
import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingServicePlugin;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.TraceRecorder;
import ghidra.async.AsyncUtils;
import ghidra.dbg.model.*;
import ghidra.dbg.target.TargetMethod;
import ghidra.dbg.target.TargetMethod.AnnotatedTargetMethod;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.*;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.program.model.address.Address;
import ghidra.program.util.ProgramLocation;
import ghidra.trace.model.Lifespan;
import ghidra.util.database.UndoableTransaction;
public class DebuggerMethodActionsPluginTest extends AbstractGhidraHeadedDebuggerGUITest {
public static final XmlSchemaContext SCHEMA_CTX;
public static final TargetObjectSchema MOD_ROOT_SCHEMA;
static {
try {
SCHEMA_CTX = XmlSchemaContext.deserialize(
EmptyDebuggerObjectModel.class.getResourceAsStream("test_schema.xml"));
SchemaBuilder builder =
new SchemaBuilder(SCHEMA_CTX, SCHEMA_CTX.getSchema(SCHEMA_CTX.name("Thread")));
SchemaName method = SCHEMA_CTX.name("Method");
builder.addAttributeSchema(
new DefaultAttributeSchema("Advance", method, true, true, true), "manual");
builder.addAttributeSchema(
new DefaultAttributeSchema("StepExt", method, true, true, true), "manual");
builder.addAttributeSchema(
new DefaultAttributeSchema("AdvanceWithFlag", method, true, true, true), "manual");
builder.addAttributeSchema(
new DefaultAttributeSchema("Between", method, true, true, true), "manual");
SCHEMA_CTX.replaceSchema(builder.build());
MOD_ROOT_SCHEMA = SCHEMA_CTX.getSchema(SCHEMA_CTX.name("Test"));
}
catch (IOException | JDOMException e) {
throw new AssertionError(e);
}
}
DebuggerListingPlugin listingPlugin;
DebuggerStaticMappingService mappingService;
DebuggerMethodActionsPlugin methodsPlugin;
List<String> commands = Collections.synchronizedList(new ArrayList<>());
@Before
public void setUpMethodAcitonsTest() throws Exception {
listingPlugin = addPlugin(tool, DebuggerListingPlugin.class);
mappingService = addPlugin(tool, DebuggerStaticMappingServicePlugin.class);
methodsPlugin = addPlugin(tool, DebuggerMethodActionsPlugin.class);
mb = new TestDebuggerModelBuilder() {
@Override
protected TestDebuggerObjectModel newModel(String typeHint) {
commands.clear();
return new TestDebuggerObjectModel(typeHint) {
@Override
public TargetObjectSchema getRootSchema() {
return MOD_ROOT_SCHEMA;
}
@Override
protected TestTargetThread newTestTargetThread(
TestTargetThreadContainer container, int tid) {
return new TestTargetThread(container, tid) {
{
changeAttributes(List.of(),
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
testModel, this),
"Initialize");
}
@TargetMethod.Export("Advance")
public CompletableFuture<Void> advance(
@TargetMethod.Param(
description = "The target address",
display = "Target",
name = "target") Address target) {
commands.add("advance(" + target + ")");
return AsyncUtils.NIL;
}
// Takes no address context
@TargetMethod.Export("StepExt")
public CompletableFuture<Void> stepExt() {
commands.add("stepExt");
return AsyncUtils.NIL;
}
// Takes a second required non-default parameter
@TargetMethod.Export("AdvanceWithFlag")
public CompletableFuture<Void> advanceWithFlag(
@TargetMethod.Param(
description = "The target address",
display = "Target",
name = "target") Address address,
@TargetMethod.Param(
description = "The flag",
display = "Flag",
name = "flag") boolean flag) {
commands.add("advanceWithFlag(" + address + "," + flag + ")");
return AsyncUtils.NIL;
}
// Takes a second address parameter
@TargetMethod.Export("Between")
public CompletableFuture<Void> between(
@TargetMethod.Param(
description = "The starting address",
display = "Start",
name = "start") Address start,
@TargetMethod.Param(
description = "The ending address",
display = "End",
name = "end") Address end) {
commands.add("between(" + start + "," + end + ")");
return AsyncUtils.NIL;
}
};
}
};
}
};
}
protected Collection<TargetMethod> collectMethods(TargetObject object) {
return object.getModel()
.getRootSchema()
.matcherForSuitable(TargetMethod.class, object.getPath())
.getCachedSuccessors(object.getModel().getModelRoot())
.values()
.stream()
.filter(o -> o instanceof TargetMethod)
.map(o -> (TargetMethod) o)
.toList();
}
@Test
public void testGetPopupActionsNoTrace() throws Throwable {
createProgram();
programManager.openProgram(program);
ProgramLocationActionContext ctx =
new ProgramLocationActionContext(listingPlugin.getProvider(), program,
new ProgramLocation(program, addr(program, 0)), null, null);
assertEquals(List.of(), methodsPlugin.getPopupActions(tool, ctx));
}
@Test
public void testGetPopupActionsNoThread() throws Throwable {
createTestModel();
recordAndWaitSync();
traceManager.openTrace(tb.trace);
traceManager.activateTrace(tb.trace);
waitForSwing();
assertEquals(4, collectMethods(mb.testThread1).size());
createProgramFromTrace(tb.trace);
programManager.openProgram(program);
ProgramLocationActionContext ctx =
new ProgramLocationActionContext(listingPlugin.getProvider(), program,
new ProgramLocation(program, addr(program, 0)), null, null);
assertEquals(List.of(), methodsPlugin.getPopupActions(tool, ctx));
}
@Test
public void testGetPopupActions() throws Throwable {
createTestModel();
TraceRecorder recorder = recordAndWaitSync();
traceManager.openTrace(tb.trace);
traceManager.activateTrace(tb.trace);
waitForSwing();
waitOn(recorder.requestFocus(mb.testThread1));
waitRecorder(recorder);
waitForSwing();
assertEquals(4, collectMethods(mb.testThread1).size());
createProgramFromTrace(tb.trace);
intoProject(program);
try (UndoableTransaction tid = UndoableTransaction.start(program, "Add memory")) {
program.getMemory()
.createInitializedBlock(".text", addr(program, 0x00400000), 0x1000,
(byte) 0, monitor, false);
}
try (UndoableTransaction tid = tb.startTransaction()) {
mappingService.addIdentityMapping(tb.trace, program, Lifespan.ALL, true);
}
waitForDomainObject(tb.trace);
waitOn(mappingService.changesSettled());
programManager.openProgram(program);
ProgramLocationActionContext ctx =
new ProgramLocationActionContext(listingPlugin.getProvider(), program,
new ProgramLocation(program, addr(program, 0x00400000)), null, null);
assertEquals(List.of("Advance"),
methodsPlugin.getPopupActions(tool, ctx).stream().map(a -> a.getName()).toList());
}
@Test
public void testMethodInvocation() throws Throwable {
createTestModel();
TraceRecorder recorder = recordAndWaitSync();
traceManager.openTrace(tb.trace);
traceManager.activateTrace(tb.trace);
waitForSwing();
waitOn(recorder.requestFocus(mb.testThread1));
waitRecorder(recorder);
waitForSwing();
assertEquals(4, collectMethods(mb.testThread1).size());
createProgramFromTrace(tb.trace);
intoProject(program);
try (UndoableTransaction tid = UndoableTransaction.start(program, "Add memory")) {
program.getMemory()
.createInitializedBlock(".text", addr(program, 0x00400000), 0x1000,
(byte) 0, monitor, false);
}
try (UndoableTransaction tid = tb.startTransaction()) {
mappingService.addIdentityMapping(tb.trace, program, Lifespan.ALL, true);
}
waitForDomainObject(tb.trace);
waitOn(mappingService.changesSettled());
programManager.openProgram(program);
ProgramLocationActionContext ctx =
new ProgramLocationActionContext(listingPlugin.getProvider(), program,
new ProgramLocation(program, addr(program, 0x00400000)), null, null);
DockingActionIf advance = Unique.assertOne(methodsPlugin.getPopupActions(tool, ctx));
assertEquals("Advance", advance.getName());
performAction(advance, ctx, true);
waitRecorder(recorder);
assertEquals(List.of("advance(00400000)"), commands);
}
}