GP-380: Better quick launch using opinions and offers.

This commit is contained in:
Dan 2021-04-21 16:54:30 -04:00
parent 93b2f5cc64
commit 808c20ab7f
28 changed files with 1210 additions and 310 deletions

View file

@ -18,21 +18,19 @@ package agent.dbgeng;
import java.util.concurrent.CompletableFuture;
import agent.dbgeng.model.impl.DbgModelImpl;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.LocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
/**
* Note this is in the testing source because it's not meant to be shipped in
* the release.... That may change if it proves stable, though, no?
* Note this is in the testing source because it's not meant to be shipped in the release.... That
* may change if it proves stable, though, no?
*/
@FactoryDescription( //
brief = "IN-VM MS dbgeng local debugger", //
htmlDetails = "Launch a dbgeng session in this same JVM" //
)
@ExtensionPointProperties(priority = 80)
public class DbgEngInJvmDebuggerModelFactory implements LocalDebuggerModelFactory {
public class DbgEngInJvmDebuggerModelFactory implements DebuggerModelFactory {
// TODO remoteTransport option?

View file

@ -18,10 +18,9 @@ package agent.dbgmodel;
import java.util.concurrent.CompletableFuture;
import agent.dbgmodel.model.impl.DbgModel2Impl;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.LocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
/**
* Note this is in the testing source because it's not meant to be shipped in the release.... That
@ -31,8 +30,7 @@ import ghidra.util.classfinder.ExtensionPointProperties;
brief = "IN-VM MS dbgmodel local debugger", //
htmlDetails = "Launch a dbgmodel session in this same JVM" //
)
@ExtensionPointProperties(priority = 70)
public class DbgModelInJvmDebuggerModelFactory implements LocalDebuggerModelFactory {
public class DbgModelInJvmDebuggerModelFactory implements DebuggerModelFactory {
@Override
public CompletableFuture<? extends DebuggerObjectModel> build() {

View file

@ -0,0 +1,51 @@
/* ###
* 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 agent.gdb;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.util.*;
import ghidra.dbg.util.ShellUtils;
public enum GdbCompatibility {
INSTANCE;
public static boolean checkGdbPresent(String path) {
try {
ProcessBuilder builder = new ProcessBuilder(path, "--version");
builder.redirectError(Redirect.INHERIT);
builder.redirectOutput(Redirect.INHERIT);
@SuppressWarnings("unused")
Process gdb = builder.start();
// TODO: Once supported versions are decided, check the version.
return true;
}
catch (IOException e) {
return false;
}
}
private final Map<String, Boolean> cache = new HashMap<>();
public boolean isCompatible(String gdbCmd) {
List<String> args = ShellUtils.parseArgs(gdbCmd);
if (args.isEmpty()) {
return false;
}
return cache.computeIfAbsent(gdbCmd, p -> checkGdbPresent(args.get(0)));
}
}

View file

@ -17,14 +17,12 @@ package agent.gdb;
import java.util.concurrent.CompletableFuture;
import agent.gdb.gadp.GdbLocalDebuggerModelFactory;
import agent.gdb.manager.GdbManager;
import agent.gdb.model.impl.GdbModelImpl;
import agent.gdb.pty.linux.LinuxPtyFactory;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.LocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
/**
* Note this is in the testing source because it's not meant to be shipped in the release.... That
@ -34,8 +32,7 @@ import ghidra.util.classfinder.ExtensionPointProperties;
brief = "IN-VM GNU gdb local debugger", //
htmlDetails = "Launch a GDB session in this same JVM" //
)
@ExtensionPointProperties(priority = 80)
public class GdbInJvmDebuggerModelFactory implements LocalDebuggerModelFactory {
public class GdbInJvmDebuggerModelFactory implements DebuggerModelFactory {
private String gdbCmd = GdbManager.DEFAULT_GDB_CMD;
@FactoryOption("GDB launch command")
@ -56,7 +53,7 @@ public class GdbInJvmDebuggerModelFactory implements LocalDebuggerModelFactory {
@Override
public boolean isCompatible() {
return GdbLocalDebuggerModelFactory.checkGdbPresent(gdbCmd);
return GdbCompatibility.INSTANCE.isCompatible(gdbCmd);
}
public String getGdbCommand() {

View file

@ -19,16 +19,15 @@ import java.util.concurrent.CompletableFuture;
import agent.gdb.model.impl.GdbModelImpl;
import agent.gdb.pty.ssh.GhidraSshPtyFactory;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.LocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
import ghidra.dbg.util.ConfigurableFactory.FactoryOption;
@FactoryDescription(
brief = "GNU gdb via SSH",
htmlDetails = "Launch a GDB session over an SSH connection")
@ExtensionPointProperties(priority = 60)
public class GdbOverSshDebuggerModelFactory implements LocalDebuggerModelFactory {
public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory {
private String gdbCmd = "gdb";
@FactoryOption("GDB launch command")

View file

@ -15,10 +15,9 @@
*/
package agent.gdb.gadp;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.util.List;
import agent.gdb.GdbCompatibility;
import agent.gdb.manager.GdbManager;
import ghidra.dbg.gadp.server.AbstractGadpLocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
@ -31,26 +30,6 @@ import ghidra.util.classfinder.ExtensionPointProperties;
)
@ExtensionPointProperties(priority = 100)
public class GdbLocalDebuggerModelFactory extends AbstractGadpLocalDebuggerModelFactory {
public static boolean checkGdbPresent(String gdbCmd) {
List<String> args = ShellUtils.parseArgs(gdbCmd);
if (args.isEmpty()) {
return false;
}
try {
ProcessBuilder builder = new ProcessBuilder(args.get(0), "--version");
builder.redirectError(Redirect.INHERIT);
builder.redirectOutput(Redirect.INHERIT);
@SuppressWarnings("unused")
Process gdb = builder.start();
// TODO: Once supported versions are decided, check the version.
return true;
}
catch (IOException e) {
return false;
}
}
protected Boolean isSuitable;
private String gdbCmd = GdbManager.DEFAULT_GDB_CMD;
@FactoryOption("GDB launch command")
@ -62,15 +41,10 @@ public class GdbLocalDebuggerModelFactory extends AbstractGadpLocalDebuggerModel
public final Property<Boolean> useExistingOption =
Property.fromAccessors(boolean.class, this::isUseExisting, this::setUseExisting);
// TODO: A factory which connects to GDB via SSH. Would need to refactor manager.
@Override
public boolean isCompatible() {
// TODO: Could potentially support GDB on Windows, but the pty thing would need porting.
if (isSuitable != null) {
return isSuitable;
}
return isSuitable = checkGdbPresent(gdbCmd);
return GdbCompatibility.INSTANCE.isCompatible(gdbCmd);
}
public String getGdbCommand() {

View file

@ -23,12 +23,13 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.LocalDebuggerModelFactory;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.gadp.client.GadpClient;
import ghidra.dbg.gadp.client.GadpTcpDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryOption;
import ghidra.util.Msg;
public abstract class AbstractGadpLocalDebuggerModelFactory implements LocalDebuggerModelFactory {
public abstract class AbstractGadpLocalDebuggerModelFactory implements DebuggerModelFactory {
public static final boolean LOG_AGENT_STDOUT = true;
protected String host = "localhost";

View file

@ -17,18 +17,16 @@ package ghidra.dbg.jdi;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.LocalDebuggerModelFactory;
import ghidra.dbg.jdi.model.JdiModelImpl;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
@FactoryDescription( //
brief = "JDI debugger", //
htmlDetails = "Debug a Java or Dalvik VM (supports JDWP)" //
)
@ExtensionPointProperties(priority = 50)
public class JdiDebuggerModelFactory implements LocalDebuggerModelFactory {
public class JdiDebuggerModelFactory implements DebuggerModelFactory {
@Override
public CompletableFuture<? extends DebuggerObjectModel> build() {

View file

@ -2,5 +2,6 @@ AutoReadMemorySpec
DebuggerBot
DebuggerMappingOpinion
DebuggerModelFactory
DebuggerProgramLaunchOpinion
DisassemblyInject
LocationTrackingSpec

View file

@ -50,8 +50,8 @@
<LI>Trace manipulation - those used for viewing and manipulating the trace database,
including machine state inspection. Most of these behave differently when the view is "at the
present," i.e., corresponds to a live target machine state. They may direct modifications to
the target, and/or request additional information from the target.</LI>
present," i.e., corresponds to a live target machine state. They may directly command and/or
request additional information from the target.</LI>
<LI>Global manipulation - those which aggregate information from several targets or traces,
presenting a comprehensive picture. Modifications in these views may be directed to any

View file

@ -24,15 +24,31 @@
<H3><A name="debug_program"></A> Debug Program</H3>
<P>This action is available whenever a program is opened, and the current program indicates an
"executable path" that exists on the local file system and is marked executable by the host
operating system. It will launch a suitable connection for debugging local applications, and
then run the current program in that debugger. If <A href=
<P>This group of actions is available whenever there exists a debug launcher that knows how to
run the current program. Various launchers may all make offers to run the current program, each
of which is presented as a item in this group. Not all offers are guaranteed to work. For
example, an offer to launch the program remotely via SSH depends on the host's availability and
the user's credentials. The offers are ordered by most recent activation. The most recent offer
used is the default one-click launcher for the current program. Each launcher may check various
conditions before making an offer. Most commonly, it will check that there is a suitable
debugger for the current program's architecture (language) on the local system, that the
program's original executable image still exists on disk, and that the user has permission to
execute it. A launcher may take any arbitrary action to run the program. Most commonly, it
starts a new connection suitable for the target, and then launches the program on that
connection. If <A href=
"help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html#record_automatically">Record
Automatically</A> is enabled, this will provide a one-click action to debug the current
program. This is similar to the <A href=
"help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html#quick_launch">Quick Launch</A>
action in the Commands and Objects window, except this one creates a new connection.</P>
action in the Commands and Objects window, except this one does not require an existing
connection.</P>
<P>The launch offers are presented in two places. First, they are listed as drop-down items
from the "Debug Program" action in the main toolbar. When activated here, there are typically
no further prompts. One notable exception is SSH, where authentication may be required. Second,
they are listed under the <SPAN class="menu">Debugger &rarr; Debug Program</SPAN> menu. When
activated here, the launcher should prompt for arguments. The chosen arguments are saved as the
default for future launches of the current program.</P>
<H3><A name="disconnect_all"></A> Disconnect All</H3>

View file

@ -42,6 +42,7 @@ import ghidra.app.plugin.core.debug.gui.target.DebuggerTargetsPlugin;
import ghidra.app.plugin.core.debug.gui.thread.DebuggerThreadsPlugin;
import ghidra.app.plugin.core.debug.gui.time.DebuggerTimePlugin;
import ghidra.app.plugin.core.debug.gui.watch.DebuggerWatchesPlugin;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer;
import ghidra.app.services.DebuggerTraceManagerService.BooleanChangeAdapter;
import ghidra.app.services.MarkerService;
import ghidra.framework.plugintool.Plugin;
@ -403,17 +404,24 @@ public interface DebuggerResources {
interface DebugProgramAction {
String NAME = "Debug Program";
String DESCRIPTION_PREFIX = "Debug ";
Icon ICON = ICON_DEBUGGER;
String GROUP = GROUP_GENERAL;
String HELP_ANCHOR = "debug_program";
static ActionBuilder builder(Plugin owner, Plugin helpOwner) {
return new ActionBuilder(NAME, owner.getName()).description(DESCRIPTION_PREFIX)
static <T> MultiStateActionBuilder<T> buttonBuilder(Plugin owner, Plugin helpOwner) {
return new MultiStateActionBuilder<T>(NAME, owner.getName())
.toolBarIcon(ICON)
.toolBarGroup(GROUP)
.menuPath(DebuggerPluginPackage.NAME, DESCRIPTION_PREFIX)
.menuIcon(ICON)
.helpLocation(new HelpLocation(helpOwner.getName(), HELP_ANCHOR));
}
static ActionBuilder menuBuilder(DebuggerProgramLaunchOffer offer, Plugin owner,
Plugin helpOwner) {
return new ActionBuilder(offer.getConfigName(), owner.getName())
.description(offer.getButtonTitle())
.menuPath(DebuggerPluginPackage.NAME, offer.getMenuParentTitle(),
offer.getMenuTitle())
.menuIcon(offer.getIcon())
.menuGroup(GROUP)
.helpLocation(new HelpLocation(helpOwner.getName(), HELP_ANCHOR));
}

View file

@ -35,7 +35,6 @@ import docking.DialogComponentProvider;
import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils;
import ghidra.dbg.target.TargetMethod;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
import ghidra.framework.plugintool.PluginTool;
@ -95,7 +94,7 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider
protected JButton invokeButton;
private final PluginTool tool;
private TargetParameterMap parameters;
private Map<String, ParameterDescription<?>> parameters;
// TODO: Not sure this is the best keying, but I think it works.
private Map<NameTypePair, Object> memorized = new HashMap<>();
@ -115,14 +114,14 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider
ntp -> parameter.defaultValue);
}
public Map<String, ?> promptArguments(TargetParameterMap parameterMap) {
public Map<String, ?> promptArguments(Map<String, ParameterDescription<?>> parameterMap) {
setParameters(parameterMap);
tool.showDialog(this);
return getArguments();
}
public void setParameters(TargetParameterMap parameterMap) {
public void setParameters(Map<String, ParameterDescription<?>> parameterMap) {
this.parameters = parameterMap;
populateOptions();
}
@ -210,13 +209,12 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider
memorized.put(NameTypePair.fromParameter(param), editor.getValue());
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public void writeConfigState(SaveState saveState) {
SaveState subState = new SaveState();
for (Map.Entry<NameTypePair, Object> ent : memorized.entrySet()) {
NameTypePair ntp = ent.getKey();
ConfigStateField.putState(subState, (Class) ntp.getType(), ntp.getName(),
ent.getValue());
ConfigStateField.putState(subState, ntp.getType().asSubclass(Object.class),
ntp.getName(), ent.getValue());
}
saveState.putXmlElement(KEY_MEMORIZED_ARGUMENTS, subState.saveToXml());
}

View file

@ -236,13 +236,12 @@ public class DebuggerConnectDialog extends DialogComponentProvider
synchronized (this) {
futureConnect = factory.build();
}
futureConnect.thenCompose(model -> {
futureConnect.thenAcceptAsync(model -> {
modelService.addModel(model);
setStatusText("");
close();
return CompletableFuture.runAsync(() -> modelService.activateModel(model),
SwingExecutorService.INSTANCE);
}).exceptionally(e -> {
modelService.activateModel(model);
}, SwingExecutorService.INSTANCE).exceptionally(e -> {
e = AsyncUtils.unwrapThrowable(e);
if (!(e instanceof CancellationException)) {
Msg.showError(this, getComponent(), "Could not connect", e);

View file

@ -0,0 +1,167 @@
/* ###
* 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.platform;
import java.util.*;
import ghidra.app.plugin.core.debug.service.model.launch.*;
import ghidra.app.services.DebuggerModelService;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.util.PathUtils;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
public class DbgDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpinion {
protected static abstract class AbstractDbgDebuggerProgramLaunchOffer
extends AbstractDebuggerProgramLaunchOffer {
public AbstractDbgDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
super(program, tool, factory);
}
@Override
public String getMenuParentTitle() {
return "Debug " + program.getName();
}
@Override
protected List<String> getLauncherPath() {
return PathUtils.parse("");
}
@Override
protected Map<String, ?> generateDefaultLauncherArgs(
Map<String, ParameterDescription<?>> params) {
return Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, program.getExecutablePath());
}
}
protected class InVmDbgengDebuggerProgramLaunchOffer
extends AbstractDbgDebuggerProgramLaunchOffer {
private static final String FACTORY_CLS_NAME =
"agent.dbgeng.DbgEngInJvmDebuggerModelFactory";
public InVmDbgengDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
super(program, tool, factory);
}
@Override
public String getConfigName() {
return "IN-VM dbgeng";
}
@Override
public String getMenuTitle() {
return "in dbgeng locally IN-VM";
}
}
protected class GadpDbgengDebuggerProgramLaunchOffer
extends AbstractDbgDebuggerProgramLaunchOffer {
private static final String FACTORY_CLS_NAME =
"agent.dbgeng.gadp.DbgEngLocalDebuggerModelFactory";
public GadpDbgengDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
super(program, tool, factory);
}
@Override
public String getConfigName() {
return "GADP dbgeng";
}
@Override
public String getMenuTitle() {
return "in dbgeng locally via GADP";
}
}
protected class InVmDbgmodelDebuggerProgramLaunchOffer
extends AbstractDbgDebuggerProgramLaunchOffer {
private static final String FACTORY_CLS_NAME =
"agent.dbgmodel.DbgModelInJvmDebuggerModelFactory";
public InVmDbgmodelDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
super(program, tool, factory);
}
@Override
public String getConfigName() {
return "IN-VM dbgmodel";
}
@Override
public String getMenuTitle() {
return "in dbgmodel locally IN-VM";
}
}
protected class GadpDbgmodelDebuggerProgramLaunchOffer
extends AbstractDbgDebuggerProgramLaunchOffer {
private static final String FACTORY_CLS_NAME =
"agent.dbgmodel.gadp.DbgModelLocalDebuggerModelFactory";
public GadpDbgmodelDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
super(program, tool, factory);
}
@Override
public String getConfigName() {
return "GADP dbgmodel";
}
@Override
public String getMenuTitle() {
return "in dbgmodel locally via GADP";
}
}
@Override
public Collection<DebuggerProgramLaunchOffer> getOffers(Program program, PluginTool tool,
DebuggerModelService service) {
String exe = program.getExecutablePath();
if (exe == null || "".equals(exe.trim())) {
return List.of();
}
List<DebuggerProgramLaunchOffer> offers = new ArrayList<>();
for (DebuggerModelFactory factory : service.getModelFactories()) {
if (!factory.isCompatible()) {
continue;
}
String clsName = factory.getClass().getName();
if (clsName.equals(InVmDbgengDebuggerProgramLaunchOffer.FACTORY_CLS_NAME)) {
offers.add(new InVmDbgengDebuggerProgramLaunchOffer(program, tool, factory));
}
else if (clsName.equals(GadpDbgengDebuggerProgramLaunchOffer.FACTORY_CLS_NAME)) {
offers.add(new GadpDbgengDebuggerProgramLaunchOffer(program, tool, factory));
}
else if (clsName.equals(InVmDbgmodelDebuggerProgramLaunchOffer.FACTORY_CLS_NAME)) {
offers.add(new InVmDbgmodelDebuggerProgramLaunchOffer(program, tool, factory));
}
else if (clsName.equals(GadpDbgmodelDebuggerProgramLaunchOffer.FACTORY_CLS_NAME)) {
offers.add(new GadpDbgmodelDebuggerProgramLaunchOffer(program, tool, factory));
}
}
return offers;
}
}

View file

@ -0,0 +1,144 @@
/* ###
* 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.platform;
import java.util.*;
import ghidra.app.plugin.core.debug.service.model.launch.*;
import ghidra.app.services.DebuggerModelService;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.util.ConfigurableFactory.Property;
import ghidra.dbg.util.PathUtils;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
public class GdbDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpinion {
protected static abstract class AbstractGdbDebuggerProgramLaunchOffer
extends AbstractDebuggerProgramLaunchOffer {
public AbstractGdbDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
super(program, tool, factory);
}
@Override
public String getMenuParentTitle() {
return "Debug " + program.getName();
}
@Override
protected List<String> getLauncherPath() {
return PathUtils.parse("Inferiors[1]");
}
@Override
protected Map<String, ?> generateDefaultLauncherArgs(
Map<String, ParameterDescription<?>> params) {
return Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, program.getExecutablePath());
}
}
protected class InVmGdbDebuggerProgramLaunchOffer
extends AbstractGdbDebuggerProgramLaunchOffer {
private static final String FACTORY_CLS_NAME = "agent.gdb.GdbInJvmDebuggerModelFactory";
public InVmGdbDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
super(program, tool, factory);
}
@Override
public String getConfigName() {
return "IN-VM GDB";
}
@Override
public String getMenuTitle() {
return "in GDB locally IN-VM";
}
}
protected class GadpGdbDebuggerProgramLaunchOffer
extends AbstractGdbDebuggerProgramLaunchOffer {
private static final String FACTORY_CLS_NAME =
"agent.gdb.gadp.GdbLocalDebuggerModelFactory";
public GadpGdbDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
super(program, tool, factory);
}
@Override
public String getConfigName() {
return "GADP GDB";
}
@Override
public String getMenuTitle() {
return "in GDB locally via GADP";
}
}
protected class SshGdbDebuggerProgramLaunchOffer extends AbstractGdbDebuggerProgramLaunchOffer {
private static final String FACTORY_CLS_NAME = "agent.gdb.GdbOverSshDebuggerModelFactory";
public SshGdbDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
super(program, tool, factory);
}
@Override
public String getConfigName() {
return "SSH GDB";
}
@Override
public String getMenuTitle() {
Map<String, Property<?>> opts = factory.getOptions();
return String.format("in GDB via ssh:%s@%s",
opts.get("SSH username").getValue(),
opts.get("SSH hostname").getValue());
}
}
@Override
public Collection<DebuggerProgramLaunchOffer> getOffers(Program program, PluginTool tool,
DebuggerModelService service) {
String exe = program.getExecutablePath();
if (exe == null || "".equals(exe.trim())) {
return List.of();
}
List<DebuggerProgramLaunchOffer> offers = new ArrayList<>();
for (DebuggerModelFactory factory : service.getModelFactories()) {
if (!factory.isCompatible()) {
continue;
}
String clsName = factory.getClass().getName();
if (clsName.equals(InVmGdbDebuggerProgramLaunchOffer.FACTORY_CLS_NAME)) {
offers.add(new InVmGdbDebuggerProgramLaunchOffer(program, tool, factory));
}
else if (clsName.equals(GadpGdbDebuggerProgramLaunchOffer.FACTORY_CLS_NAME)) {
offers.add(new GadpGdbDebuggerProgramLaunchOffer(program, tool, factory));
}
else if (clsName.equals(SshGdbDebuggerProgramLaunchOffer.FACTORY_CLS_NAME)) {
offers.add(new SshGdbDebuggerProgramLaunchOffer(program, tool, factory));
}
}
return offers;
}
}

View file

@ -15,7 +15,7 @@
*/
package ghidra.app.plugin.core.debug.service.model;
import static ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import static ghidra.app.plugin.core.debug.gui.DebuggerResources.showError;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
@ -24,6 +24,7 @@ import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
@ -36,6 +37,8 @@ import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.DisconnectAllAction;
import ghidra.app.plugin.core.debug.mapping.*;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOpinion;
import ghidra.app.services.*;
import ghidra.async.AsyncFence;
import ghidra.dbg.*;
@ -48,6 +51,7 @@ import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.lifecycle.Internal;
import ghidra.program.model.listing.Program;
import ghidra.trace.database.DBTrace;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
@ -57,7 +61,14 @@ import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.datastruct.CollectionChangeListener;
import ghidra.util.datastruct.ListenerSet;
@PluginInfo(shortDescription = "Debugger models manager service", description = "Manage debug sessions, connections, and trace recording", category = PluginCategoryNames.DEBUGGER, packageName = DebuggerPluginPackage.NAME, status = PluginStatus.HIDDEN, servicesRequired = {}, servicesProvided = {
@PluginInfo(
shortDescription = "Debugger models manager service",
description = "Manage debug sessions, connections, and trace recording",
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.HIDDEN,
servicesRequired = {},
servicesProvided = {
DebuggerModelService.class, })
public class DebuggerModelServicePlugin extends Plugin
implements DebuggerModelServiceInternal, FrontEndOnly {
@ -298,28 +309,6 @@ public class DebuggerModelServicePlugin extends Plugin
return true;
}
protected LocalDebuggerModelFactory getDefaultLocalDebuggerModelFactory() {
return factories.stream()
.filter(LocalDebuggerModelFactory.class::isInstance)
.map(LocalDebuggerModelFactory.class::cast)
.sorted(Comparator.comparing(f -> -f.getPriority()))
.filter(LocalDebuggerModelFactory::isCompatible)
.findFirst()
.orElse(null);
}
@Override
public CompletableFuture<? extends DebuggerObjectModel> startLocalSession() {
LocalDebuggerModelFactory factory = getDefaultLocalDebuggerModelFactory();
if (factory == null) {
return CompletableFuture.failedFuture(
new NoSuchElementException("No suitable launcher for the local platform"));
}
CompletableFuture<? extends DebuggerObjectModel> future = factory.build();
future.thenAccept(this::addModel);
return future;
}
@Override
public TraceRecorder recordTarget(TargetObject target, DebuggerTargetTraceMapper mapper)
throws IOException {
@ -677,4 +666,11 @@ public class DebuggerModelServicePlugin extends Plugin
}
}
}
@Override
public Stream<DebuggerProgramLaunchOffer> getProgramLaunchOffers(Program program) {
return ClassSearcher.getInstances(DebuggerProgramLaunchOpinion.class)
.stream()
.flatMap(opinion -> opinion.getOffers(program, tool, this).stream());
}
}

View file

@ -19,9 +19,15 @@ import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.builder.MultiStateActionBuilder;
import docking.menu.ActionState;
import docking.menu.MultiStateDockingAction;
import docking.widgets.EventTrigger;
import ghidra.app.events.ProgramActivatedPluginEvent;
import ghidra.app.events.ProgramClosedPluginEvent;
import ghidra.app.plugin.PluginCategoryNames;
@ -29,20 +35,26 @@ import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.DebugProgramAction;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.DisconnectAllAction;
import ghidra.app.plugin.core.debug.mapping.DebuggerTargetTraceMapper;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer;
import ghidra.app.plugin.core.debug.utils.BackgroundUtils;
import ghidra.app.services.*;
import ghidra.async.SwingExecutorService;
import ghidra.dbg.*;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetThread;
import ghidra.framework.main.AppInfo;
import ghidra.framework.main.FrontEndTool;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.ProgramUserData;
import ghidra.program.model.util.StringPropertyMap;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
import ghidra.util.Msg;
import ghidra.util.database.UndoableTransaction;
import ghidra.util.datastruct.CollectionChangeListener;
import ghidra.util.datastruct.ListenerSet;
import ghidra.util.task.TaskMonitor;
@ -66,6 +78,39 @@ import ghidra.util.task.TaskMonitor;
public class DebuggerModelServiceProxyPlugin extends Plugin
implements DebuggerModelServiceInternal {
private static final String KEY_MOST_RECENT_LAUNCHES = "mostRecentLaunches";
private static final DebuggerProgramLaunchOffer DUMMY_LAUNCH_OFFER =
new DebuggerProgramLaunchOffer() {
@Override
public CompletableFuture<Void> launchProgram(TaskMonitor monitor, boolean prompt) {
throw new AssertionError("Who clicked me?");
}
@Override
public String getConfigName() {
return "DUMMY";
}
@Override
public String getMenuParentTitle() {
return null;
}
@Override
public String getMenuTitle() {
return null;
}
@Override
public String getButtonTitle() {
return "No quick launcher for the current program";
}
};
private static final ActionState<DebuggerProgramLaunchOffer> DUMMY_LAUNCH_STATE =
new ActionState<>(DUMMY_LAUNCH_OFFER.getButtonTitle(), DUMMY_LAUNCH_OFFER.getIcon(),
DUMMY_LAUNCH_OFFER);
protected static DebuggerModelServicePlugin getOrCreateFrontEndDelegate() {
FrontEndTool frontEnd = AppInfo.getFrontEndTool();
for (Plugin plugin : frontEnd.getManagedPlugins()) {
@ -161,7 +206,8 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
protected final ProxiedRecorderChangeListener recorderChangeListener =
new ProxiedRecorderChangeListener();
DockingAction actionDebugProgram;
MultiStateDockingAction<DebuggerProgramLaunchOffer> actionDebugProgram;
Set<DockingAction> actionDebugProgramMenus = new HashSet<>();
DockingAction actionDisconnectAll;
protected final ListenerSet<CollectionChangeListener<DebuggerModelFactory>> factoryListeners =
@ -189,9 +235,14 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
protected void createActions() {
// Note, I have to give an enabledWhen, otherwise any context change re-enables it
actionDebugProgram = DebugProgramAction.builder(this, delegate)
.enabledWhen(ctx -> currentProgramPath != null)
.onAction(this::debugProgramActivated)
MultiStateActionBuilder<DebuggerProgramLaunchOffer> builderDebugProgram =
DebugProgramAction.buttonBuilder(this, delegate);
actionDebugProgram = builderDebugProgram
.enabledWhen(ctx -> currentProgram != null)
.onAction(this::debugProgramButtonActivated)
.onActionStateChanged(this::debugProgramStateActivated)
.performActionOnButtonClick(true)
.addState(DUMMY_LAUNCH_STATE)
.buildAndInstall(tool);
actionDisconnectAll = DisconnectAllAction.builder(this, delegate)
.menuPath("Debugger", DisconnectAllAction.NAME)
@ -201,58 +252,139 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
updateActionDebugProgram();
}
private void debugProgramActivated(ActionContext ctx) {
if (currentProgramPath == null) {
return;
}
/**
* Note the background task must have an object for a "transaction", even though this
* particular task doesn't actually touch the program. Annoying.
*/
BackgroundUtils.async(tool, currentProgram, actionDebugProgram.getDescription(), true, true,
true, this::debugProgram);
}
private void activatedDisconnectAll(ActionContext context) {
closeAllModels();
}
private CompletableFuture<Void> debugProgram(Program __, TaskMonitor monitor) {
monitor.initialize(3);
monitor.setMessage("Starting local session");
return startLocalSession().thenCompose(model -> {
CompletableFuture<Void> swing = CompletableFuture.runAsync(() -> {
// Needed to auto-record via objects provider
activateModel(model);
}, SwingExecutorService.INSTANCE);
return swing.thenCompose(___ -> model.fetchModelRoot());
}).thenCompose(root -> {
monitor.incrementProgress(1);
monitor.setMessage("Finding launcher");
CompletableFuture<? extends TargetLauncher> futureLauncher =
DebugModelConventions.findSuitable(TargetLauncher.class, root);
return futureLauncher;
}).thenCompose(launcher -> {
monitor.incrementProgress(1);
monitor.setMessage("Launching " + currentProgramPath);
// TODO: Pluggable ways to populate this
// TODO: Maybe still prompt the user?
// TODO: Launch configurations, like Eclipse?
// TODO: Maybe just let the pluggable thing invoke launch itself
return launcher.launch(
Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, currentProgramPath.toString()));
@Override
public Stream<DebuggerProgramLaunchOffer> getProgramLaunchOffers(Program program) {
return orderOffers(delegate.getProgramLaunchOffers(program), program);
}
protected List<String> readMostRecentLaunches(Program program) {
StringPropertyMap prop = program.getProgramUserData()
.getStringProperty(getName(), KEY_MOST_RECENT_LAUNCHES, false);
if (prop == null) {
return List.of();
}
Address min = program.getAddressFactory().getDefaultAddressSpace().getMinAddress();
String str = prop.getString(min);
if (str == null) {
return List.of();
}
return List.of(str.split(";"));
}
protected void writeMostRecentLaunches(Program program, List<String> mrl) {
ProgramUserData userData = program.getProgramUserData();
try (UndoableTransaction tid = UndoableTransaction.start(userData)) {
StringPropertyMap prop = userData
.getStringProperty(getName(), KEY_MOST_RECENT_LAUNCHES, true);
Address min = program.getAddressFactory().getDefaultAddressSpace().getMinAddress();
prop.add(min, mrl.stream().collect(Collectors.joining(";")));
}
}
static class OfferComparator implements Comparator<DebuggerProgramLaunchOffer> {
Map<String, Integer> fastIndex = new HashMap<>();
public OfferComparator(List<String> mostRecentNames) {
int i = 0;
for (String name : mostRecentNames) {
fastIndex.put(name, i++);
}
}
@Override
public int compare(DebuggerProgramLaunchOffer o1, DebuggerProgramLaunchOffer o2) {
int i1 = fastIndex.getOrDefault(o1, -1);
int i2 = fastIndex.getOrDefault(o2, -1);
int result = i1 - i2; // reversed, yes. Most recent is last in list
if (result != 0) {
return result;
}
return o1.defaultPriority() - o2.defaultPriority(); // Greater is higher priority
}
}
protected Stream<DebuggerProgramLaunchOffer> orderOffers(
Stream<DebuggerProgramLaunchOffer> offers, Program program) {
List<String> mrl = readMostRecentLaunches(program);
return offers.sorted(Comparator.comparingInt(o -> -mrl.indexOf(o.getConfigName())));
}
private void debugProgram(DebuggerProgramLaunchOffer offer, Program program, boolean prompt) {
BackgroundUtils.async(tool, program, offer.getButtonTitle(), true, true, true, (p, m) -> {
List<String> mrl = new ArrayList<>(readMostRecentLaunches(program));
mrl.remove(offer.getConfigName());
mrl.add(offer.getConfigName());
writeMostRecentLaunches(program, mrl);
CompletableFuture.runAsync(() -> {
updateActionDebugProgram();
}, AsyncUtils.SWING_EXECUTOR).exceptionally(ex -> {
Msg.error(this, "Trouble writing recent launches to program user data");
return null;
});
return offer.launchProgram(m, prompt);
});
}
private void debugProgramButtonActivated(ActionContext ctx) {
DebuggerProgramLaunchOffer offer = actionDebugProgram.getCurrentUserData();
Program program = currentProgram;
if (offer == null || program == null) {
return;
}
debugProgram(offer, program, false);
}
private void debugProgramStateActivated(ActionState<DebuggerProgramLaunchOffer> offer,
EventTrigger trigger) {
if (trigger == EventTrigger.GUI_ACTION) {
debugProgramButtonActivated(null);
}
}
private void debugProgramMenuActivated(DebuggerProgramLaunchOffer offer) {
Program program = currentProgram;
if (program == null) {
return;
}
debugProgram(offer, program, true);
}
private void updateActionDebugProgram() {
if (actionDebugProgram == null) {
return;
}
actionDebugProgram.setEnabled(currentProgramPath != null);
String desc = currentProgramPath == null ? DebugProgramAction.DESCRIPTION_PREFIX.trim()
: DebugProgramAction.DESCRIPTION_PREFIX + currentProgramPath;
actionDebugProgram.setDescription(desc);
actionDebugProgram.getMenuBarData().setMenuItemName(desc);
Program program = currentProgram;
List<DebuggerProgramLaunchOffer> offers = program == null ? List.of()
: getProgramLaunchOffers(program).collect(Collectors.toList());
List<ActionState<DebuggerProgramLaunchOffer>> states = offers.stream()
.map(o -> new ActionState<DebuggerProgramLaunchOffer>(o.getButtonTitle(),
o.getIcon(), o))
.collect(Collectors.toList());
if (!states.isEmpty()) {
actionDebugProgram.setActionStates(states);
actionDebugProgram.setEnabled(true);
actionDebugProgram.setCurrentActionState(states.get(0));
}
else {
actionDebugProgram.setActionStates(List.of(DUMMY_LAUNCH_STATE));
actionDebugProgram.setEnabled(false);
actionDebugProgram.setCurrentActionState(DUMMY_LAUNCH_STATE);
}
for (Iterator<DockingAction> it = actionDebugProgramMenus.iterator(); it.hasNext();) {
DockingAction action = it.next();
it.remove();
tool.removeAction(action);
}
for (DebuggerProgramLaunchOffer offer : offers) {
actionDebugProgramMenus.add(DebugProgramAction.menuBuilder(offer, this, delegate)
.onAction(ctx -> debugProgramMenuActivated(offer))
.buildAndInstall(tool));
}
}
@Override
@ -349,11 +481,6 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
return delegate.removeModel(model);
}
@Override
public CompletableFuture<? extends DebuggerObjectModel> startLocalSession() {
return delegate.startLocalSession();
}
@Override
public TraceRecorder recordTarget(TargetObject target, DebuggerTargetTraceMapper mapper)
throws IOException {

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.service.model.launch;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.jdom.Element;
import org.jdom.JDOMException;
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServicePlugin;
import ghidra.app.services.DebuggerModelService;
import ghidra.async.SwingExecutorService;
import ghidra.dbg.*;
import ghidra.dbg.target.TargetLauncher;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.util.PathUtils;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginUtils;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.ProgramUserData;
import ghidra.program.model.util.StringPropertyMap;
import ghidra.util.Msg;
import ghidra.util.database.UndoableTransaction;
import ghidra.util.task.TaskMonitor;
import ghidra.util.xml.XmlUtilities;
public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProgramLaunchOffer {
protected final Program program;
protected final PluginTool tool;
protected final DebuggerModelFactory factory;
public AbstractDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
this.program = program;
this.tool = tool;
this.factory = factory;
}
protected List<String> getLauncherPath() {
return PathUtils.parse("");
}
private void saveLauncherArgs(Map<String, ?> args,
Map<String, ParameterDescription<?>> params) {
SaveState state = new SaveState();
for (ParameterDescription<?> param : params.values()) {
Object val = args.get(param.name);
if (val != null) {
ConfigStateField.putState(state, param.type.asSubclass(Object.class), param.name,
val);
}
}
String owner = PluginUtils.getPluginNameFromClass(DebuggerModelServicePlugin.class);
ProgramUserData userData = program.getProgramUserData();
try (UndoableTransaction tid = UndoableTransaction.start(userData)) {
StringPropertyMap stringProperty =
userData.getStringProperty(owner, getConfigName(), true);
Element element = state.saveToXml();
stringProperty.add(Address.NO_ADDRESS, XmlUtilities.toString(element));
}
}
protected Map<String, ?> takeDefaultsForParameters(
Map<String, ParameterDescription<?>> params) {
return params.values().stream().collect(Collectors.toMap(p -> p.name, p -> p.defaultValue));
}
/**
* Generate the default launcher arguments
*
* <p>
* It is not sufficient to simply take the defaults specified in the parameters. This must
* populate the arguments necessary to launch the requested program.
*
* @param params the parameters
* @return the default arguments
*/
protected abstract Map<String, ?> generateDefaultLauncherArgs(
Map<String, ParameterDescription<?>> params);
/**
* Prompt the user for arguments, showing those last used or defaults
*
* @param params the parameters of the model's launcher
* @return the arguments given by the user
*/
protected Map<String, ?> promptLauncherArgs(Map<String, ParameterDescription<?>> params) {
DebuggerMethodInvocationDialog dialog =
new DebuggerMethodInvocationDialog(tool, getButtonTitle(), "Launch", getIcon());
// NB. Do not invoke read/writeConfigState
Map<String, ?> args = loadLastLauncherArgs(params, true);
for (ParameterDescription<?> param : params.values()) {
Object val = args.get(param.name);
if (val != null) {
dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class), val);
}
}
args = dialog.promptArguments(params);
saveLauncherArgs(args, params);
return args;
}
/**
* Load the arguments last used for this offer, or give the defaults
*
* <p>
* If there are no saved "last used" arguments, then this will return the defaults. If there are
* saved arguments, but they cannot be loaded, then this will behave differently depending on
* whether the user will be confirming the arguments. If there will be no prompt/confirmation,
* then this method must throw an exception in order to avoid launching with defaults, when the
* user may be expecting a customized launch. If there will be a prompt, then this may safely
* return the defaults, since the user will be given a chance to correct them.
*
* @param params the parameters of the model's launcher
* @param forPrompt true if the user will be confirming the arguments
* @return the loaded arguments, or defaults
*/
protected Map<String, ?> loadLastLauncherArgs(
Map<String, ParameterDescription<?>> params, boolean forPrompt) {
/**
* TODO: Supposedly, per-program, per-user config stuff is being generalized for analyzers.
* Re-examine this if/when that gets merged
*/
String owner = PluginUtils.getPluginNameFromClass(DebuggerModelServicePlugin.class);
ProgramUserData userData = program.getProgramUserData();
StringPropertyMap property =
userData.getStringProperty(owner, getConfigName(), false);
if (property != null) {
String xml = property.getString(Address.NO_ADDRESS);
if (xml != null) {
try {
Element element = XmlUtilities.fromString(xml);
SaveState state = new SaveState(element);
Map<String, Object> args = new LinkedHashMap<>();
for (ParameterDescription<?> param : params.values()) {
args.put(param.name,
ConfigStateField.getState(state, param.type, param.name));
}
return args;
}
catch (JDOMException | IOException e) {
if (!forPrompt) {
throw new RuntimeException(
"Saved launcher args are corrupt, or launcher parameters changed. Not launching.",
e);
}
Msg.error(this,
"Saved launcher args are corrup, or launcher parameters changed. Defaulting.",
e);
}
}
}
Map<String, ?> args = generateDefaultLauncherArgs(params);
saveLauncherArgs(args, params);
return args;
}
/**
* Obtain the launcher args
*
* <p>
* This should either call {@link #promptLauncherArgs(Map))} or
* {@link #loadLastLauncherArgs(Map, boolean))}. Note if choosing the latter, the user will not
* be prompted to confirm.
*
* @param params the parameters of the model's launcher
* @return the chosen arguments
*/
protected Map<String, ?> getLauncherArgs(Map<String, ParameterDescription<?>> params,
boolean prompt) {
return prompt
? promptLauncherArgs(params)
: loadLastLauncherArgs(params, false);
}
/**
* Get the model factory, as last configured by the user, for this launcher
*
* @return the factory
*/
protected DebuggerModelFactory getModelFactory() {
return factory;
}
/**
* TODO: This could be more surgical, and perhaps ought to be part of
* {@link DebugModelConventions}.
*/
static class ValueExpecter extends CompletableFuture<Object> implements DebuggerModelListener {
private final DebuggerObjectModel model;
private final List<String> path;
public ValueExpecter(DebuggerObjectModel model, List<String> path) {
this.model = model;
this.path = path;
model.addModelListener(this);
retryFetch();
}
protected void retryFetch() {
model.fetchModelValue(path).thenAccept(v -> {
if (v != null) {
model.removeModelListener(this);
complete(v);
}
}).exceptionally(ex -> {
model.removeModelListener(this);
completeExceptionally(ex);
return null;
});
}
@Override
public void rootAdded(TargetObject root) {
retryFetch();
}
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
retryFetch();
}
@Override
public void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
retryFetch();
}
}
@Override
public CompletableFuture<Void> launchProgram(TaskMonitor monitor, boolean prompt) {
monitor.initialize(2);
monitor.setMessage("Connecting");
return getModelFactory().build().thenApplyAsync(m -> {
DebuggerModelService service = tool.getService(DebuggerModelService.class);
service.addModel(m);
return m;
}).thenComposeAsync(m -> {
List<String> launcherPath = getLauncherPath();
TargetObjectSchema schema = m.getRootSchema().getSuccessorSchema(launcherPath);
if (!schema.getInterfaces().contains(TargetLauncher.class)) {
throw new AssertionError("LaunchOffer / model implementation error: " +
"The given launcher path is not a TargetLauncher, according to its schema");
}
return new ValueExpecter(m, launcherPath);
}, SwingExecutorService.INSTANCE).thenCompose(l -> {
monitor.incrementProgress(1);
monitor.setMessage("Launching");
TargetLauncher launcher = (TargetLauncher) l;
return launcher.launch(getLauncherArgs(launcher.getParameters(), prompt));
}).thenRun(() -> {
monitor.incrementProgress(1);
});
}
}

View file

@ -0,0 +1,109 @@
/* ###
* 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.service.model.launch;
import java.util.concurrent.CompletableFuture;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.util.task.TaskMonitor;
/**
* An offer to launch a program with a given mechanism
*
* <p>
* Typically each offer is configured with the program it's going to launch, and knows how to work a
* specific connector and platform to obtain a target executing the program's image. The mechanisms
* may vary wildly from platform to platform.
*/
public interface DebuggerProgramLaunchOffer {
/**
* Launch the program using the offered mechanism
*
* @param monitor a monitor for progress and cancellation
* @param prompt if the user should be prompted to confirm launch parameters
* @return a future which completes when the program is launched
*/
CompletableFuture<Void> launchProgram(TaskMonitor monitor, boolean prompt);
/**
* A name so that this offer can be recognized later
*
* <p>
* The name is saved to configuration files, so that user preferences and priorities can be
* memorized. The opinion will generate each offer fresh each time, so it's important that the
* "same offer" have the same configuration name. Note that the name <em>cannot</em> depend on
* the program name, but can depend on the model factory and program language and/or compiler
* spec. This name cannot contain semicolons ({@ code ;}).
*
* @return the configuration name
*/
String getConfigName();
/**
* Get the icon displayed in the UI for this offer
*
* <p>
* Don't override this except for good reason. If you do override, please return a variant that
* still resembles this icon, e.g., just overlay on this one.
*
* @return the icon
*/
default Icon getIcon() {
return DebuggerResources.ICON_DEBUGGER;
}
/**
* Get the text display on the parent menu for this offer
*
* <p>
* Unless there's good reason, this should always be "Debug [executablePath]".
*
* @return the title
*/
String getMenuParentTitle();
/**
* Get the text displayed on the menu for this offer
*
* @return the title
*/
String getMenuTitle();
/**
* Get the text displayed on buttons for this offer
*
* @return the title
*/
default String getButtonTitle() {
return getMenuParentTitle() + " " + getMenuTitle();
}
/**
* Get the default priority (position in the menu) of the offer
*
* <p>
* Note that greater priorities will be listed first, with the greatest being the default "quick
* launch" offer.
*
* @return the priority
*/
default int defaultPriority() {
return 50;
}
}

View file

@ -0,0 +1,28 @@
/* ###
* 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.service.model.launch;
import java.util.Collection;
import ghidra.app.services.DebuggerModelService;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.util.classfinder.ExtensionPoint;
public interface DebuggerProgramLaunchOpinion extends ExtensionPoint {
Collection<DebuggerProgramLaunchOffer> getOffers(Program program, PluginTool tool,
DebuggerModelService service);
}

View file

@ -19,20 +19,25 @@ import java.io.IOException;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import ghidra.app.plugin.core.debug.mapping.DebuggerMappingOpinion;
import ghidra.app.plugin.core.debug.mapping.DebuggerTargetTraceMapper;
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceProxyPlugin;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.*;
import ghidra.framework.plugintool.PluginEvent;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
import ghidra.util.datastruct.CollectionChangeListener;
@ServiceInfo(defaultProvider = DebuggerModelServiceProxyPlugin.class, description = "Service for managing debug sessions and connections")
@ServiceInfo(
defaultProvider = DebuggerModelServiceProxyPlugin.class,
description = "Service for managing debug sessions and connections")
public interface DebuggerModelService {
/**
* Get the set of model factories found on the classpath
@ -58,6 +63,7 @@ public interface DebuggerModelService {
/**
* Get the set of active recorders
*
* <p>
* A recorder is active as long as its target (usually a process) is valid. It becomes inactive
* when the target becomes invalid, or when the user stops the recording.
*
@ -68,6 +74,7 @@ public interface DebuggerModelService {
/**
* Register a model with this service
*
* <p>
* In general, the tool will only display models registered here
*
* @param model the model to register
@ -84,26 +91,18 @@ public interface DebuggerModelService {
*/
boolean removeModel(DebuggerObjectModel model);
/**
* Start and connect to a suitable debugger on the local system
*
* In most circumstances, this will start a local GADP agent compatible with the local operating
* system. It will then connect to it via localhost, and register the resulting model with this
* service.
*
* @return a future which completes upon successful session creation.
*/
CompletableFuture<? extends DebuggerObjectModel> startLocalSession();
/**
* Start a new trace on the given target
*
* <p>
* Following conventions, the target must be a container, usually a process. Ideally, the model
* will present the process as having memory, modules, and threads; and the model will present
* each thread as having registers, or a stack with frame 0 presenting the registers.
*
* <p>
* Any given container can be traced by at most one recorder.
*
* <p>
* TODO: If mappers remain bound to a prospective target, then remove target from the parameters
* here.
*
@ -119,6 +118,7 @@ public interface DebuggerModelService {
/**
* Query mapping opinions and record the given target using the "best" offer
*
* <p>
* If exactly one offer is given, this simply uses it. If multiple are given, this automatically
* chooses the "best" one without prompting the user. If none are given, this fails.
*
@ -131,10 +131,12 @@ public interface DebuggerModelService {
/**
* Query mapping opinions, prompt the user, and record the given target
*
* <p>
* Even if exactly one offer is given, the user is prompted to provide information about the new
* recording, and to give the user an opportunity to cancel. If none are given, the prompt says
* as much. If the user cancels, the returned future completes with {@code null}.
*
* <p>
* TODO: Should the prompt allow the user to force an opinion which gave no offers?
*
* @see DebuggerMappingOpinion#queryOpinions(TargetObject)
@ -146,6 +148,7 @@ public interface DebuggerModelService {
/**
* Start and open a new trace on the given target
*
* <p>
* Starts a new trace, and opens it in the tool
*
* @see #recordTarget(TargetObject)
@ -180,8 +183,10 @@ public interface DebuggerModelService {
/**
* Get the object (usually a process) associated with the given destination trace
*
* <p>
* A recorder uses conventions to discover the "process" in the model, given a target object.
*
* <p>
* TODO: Conventions for targets other than processes are not yet specified.
*
* @param trace the destination trace
@ -200,11 +205,13 @@ public interface DebuggerModelService {
/**
* Get the object associated with the given destination trace thread
*
* <p>
* A recorder uses conventions to discover "threads" for a given target object, usually a
* process. Those threads are then assigned to corresponding destination trace threads. Assuming
* the given trace thread is the destination of an active recorder, this method finds the
* corresponding model "thread."
*
* <p>
* TODO: Conventions for targets other than processes (containing threads) are not yet
* specified.
*
@ -216,6 +223,7 @@ public interface DebuggerModelService {
/**
* Get the destination trace thread, if applicable, for a given source thread
*
* <p>
* Consider {@link #getTraceThread(TargetObject, TargetExecutionStateful)} if the caller already
* has a handle to the thread's container.
*
@ -227,6 +235,7 @@ public interface DebuggerModelService {
/**
* Get the destination trace thread, if applicable, for a given source thread
*
* <p>
* This method is slightly faster than {@link #getTraceThread(TargetExecutionStateful)}, since
* it doesn't have to search for the applicable recorder. However, if the wrong container is
* given, this method will fail to find the given thread.
@ -254,6 +263,7 @@ public interface DebuggerModelService {
/**
* Get the last focused object related to the given target
*
* <p>
* Assuming the target object is being actively traced, find the last focused object among those
* being traced by the same recorder. Essentially, given that the target likely belongs to a
* process, find the object within that process that last had focus. This is primarily used when
@ -268,6 +278,7 @@ public interface DebuggerModelService {
/**
* Listen for changes in available model factories
*
* <p>
* The caller must keep a strong reference to the listener, or it will be automatically removed.
*
* @param listener the listener
@ -284,8 +295,10 @@ public interface DebuggerModelService {
/**
* Listen for changes in registered models
*
* <p>
* The caller must beep a strong reference to the listener, or it will be automatically removed.
*
* <p>
* TODO: Probably replace this with a {@link PluginEvent}
*
* @param listener the listener
@ -295,6 +308,7 @@ public interface DebuggerModelService {
/**
* Remove a listener for changes in registered models
*
* <p>
* TODO: Probably replace this with a {@link PluginEvent}
*
* @param listener the listener
@ -304,8 +318,10 @@ public interface DebuggerModelService {
/**
* Listen for changes in active trace recorders
*
* <p>
* The caller must beep a strong reference to the listener, or it will be automatically removed.
*
* <p>
* TODO: Probably replace this with a {@link PluginEvent}
*
* @param listener the listener
@ -315,9 +331,18 @@ public interface DebuggerModelService {
/**
* Remove a listener for changes in active trace recorders
*
* <p>
* TODO: Probably replace this with a {@link PluginEvent}
*
* @param listener the listener
*/
void removeTraceRecordersChangedListener(CollectionChangeListener<TraceRecorder> listener);
/**
* Collect all offers for launching the given program
*
* @param program the program to launch
* @return the offers
*/
Stream<DebuggerProgramLaunchOffer> getProgramLaunchOffers(Program program);
}

View file

@ -19,20 +19,21 @@ import static org.junit.Assert.*;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.junit.Test;
import generic.Unique;
import ghidra.app.plugin.core.debug.event.ModelObjectFocusedPluginEvent;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.plugin.core.debug.service.model.TestDebuggerProgramLaunchOpinion.TestDebuggerProgramLaunchOffer;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer;
import ghidra.app.services.TraceRecorder;
import ghidra.async.AsyncPairingQueue;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.model.TestDebuggerObjectModel;
import ghidra.dbg.model.TestLocalDebuggerModelFactory;
import ghidra.dbg.model.TestDebuggerModelFactory;
import ghidra.dbg.testutil.DebuggerModelTestUtils;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
@ -145,6 +146,17 @@ public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITes
};
}
@Test
public void testGetProgramLaunchOffers() throws Exception {
createAndOpenProgramWithExePath("/my/fun/path");
TestDebuggerModelFactory factory = new TestDebuggerModelFactory();
modelServiceInternal.setModelFactories(List.of(factory));
List<DebuggerProgramLaunchOffer> offers =
modelService.getProgramLaunchOffers(program).collect(Collectors.toList());
DebuggerProgramLaunchOffer offer = Unique.assertOne(offers);
assertEquals(TestDebuggerProgramLaunchOffer.class, offer.getClass());
}
@Test
public void testGetModels() throws Exception {
assertEquals(Set.of(), modelService.getModels());
@ -235,21 +247,6 @@ public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITes
};
}
@Test
public void testStartLocalSession() throws Exception {
TestLocalDebuggerModelFactory factory = new TestLocalDebuggerModelFactory();
modelServiceInternal.setModelFactories(List.of(factory));
CompletableFuture<? extends DebuggerObjectModel> futureSession =
modelService.startLocalSession();
TestDebuggerObjectModel model = new TestDebuggerObjectModel();
assertEquals(Set.of(), modelService.getModels());
factory.pollBuild().complete(model);
futureSession.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
assertEquals(Set.of(model), modelService.getModels());
}
@Test
public void testRecordThenCloseStopsRecording() throws Throwable {
createTestModel();

View file

@ -0,0 +1,67 @@
/* ###
* 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.service.model;
import static org.junit.Assert.assertEquals;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import generic.Unique;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOpinion;
import ghidra.app.services.DebuggerModelService;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.model.TestDebuggerModelFactory;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.util.task.TaskMonitor;
public class TestDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpinion {
static class TestDebuggerProgramLaunchOffer implements DebuggerProgramLaunchOffer {
@Override
public CompletableFuture<Void> launchProgram(TaskMonitor monitor, boolean prompt) {
return AsyncUtils.NIL;
}
@Override
public String getConfigName() {
return "TEST";
}
@Override
public String getMenuParentTitle() {
return "Debug it";
}
@Override
public String getMenuTitle() {
return "in Fake Debugger";
}
}
@Override
public Collection<DebuggerProgramLaunchOffer> getOffers(Program program, PluginTool tool,
DebuggerModelService service) {
DebuggerModelFactory factory = Unique.assertOne(service.getModelFactories());
assertEquals(TestDebuggerModelFactory.class, factory.getClass());
return List.of(new TestDebuggerProgramLaunchOffer());
}
}

View file

@ -342,22 +342,6 @@ public interface DebuggerObjectModel {
* are refreshed; and {@code A}'s, {@code B[1]}'s, and {@code C[2]}'s attribute caches are
* refreshed.
*
* @implNote The returned value cannot be a {@link TargetObjectRef} unless the value represents
* a link. In other words, if the path refers to an object, the model must return the
* object, not a ref. When the value is a link, the implementation may optionally
* resolve the object, but should only do so if it doesn't incur a significant cost.
* Furthermore, such links cannot be resolved -- though they can be substituted for
* the target object at the linked path. In other words, the path of the returned ref
* (or object) must represent the link's target. Suppose {@code A[1]} is a link to
* {@code B[1]}, which is in turn a link to {@code C[1]} -- honestly, linked links
* ought to be a rare occurrence -- then fetching {@code A[1]} must return a ref to
* {@code B[1]}. It must not return {@code C[1]} nor a ref to it. The reason deals
* with caching and updates. If a request for {@code A[1]} were to return
* {@code C[1]}, a client may cache that result. Suppose that client then observes a
* change causing {@code B[1]} to link to {@code C[2]}. This implies that {@code A[1]}
* now resolves to {@code C[2]}; however, the client has not received enough
* information to update or invalidate its cache.
*
* @param path the path
* @param refresh true to refresh caches
* @return the found value, or {@code null} if it does not exist

View file

@ -1,44 +0,0 @@
/* ###
* 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.dbg;
import ghidra.util.classfinder.ExtensionPointProperties;
/**
* A factory for a local debugger model
*
* <p>
* These factories are searched when attempting to create a new default debug model targeting the
* local environment.
*/
public interface LocalDebuggerModelFactory extends DebuggerModelFactory {
/**
* Get the priority of this factory
*
* <p>
* In the event multiple compatible factories are discovered, the one with the highest priority
* is selected, breaking ties arbitrarily.
*
* <p>
* The default implementation returns the priority given by {@link ExtensionPointProperties}. If
* the priority must be determined dynamically, then override this implementation.
*
* @return the priority, where lower values indicate higher priority.
*/
default int getPriority() {
return ExtensionPointProperties.Util.getPriority(getClass());
}
}

View file

@ -1,44 +0,0 @@
/* ###
* 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.dbg.model;
import java.util.Deque;
import java.util.LinkedList;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.LocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
@FactoryDescription(brief = "Mocked Local Client", htmlDetails = TestDebuggerModelFactory.FAKE_DETAILS)
public class TestLocalDebuggerModelFactory implements LocalDebuggerModelFactory {
protected final Deque<CompletableFuture<DebuggerObjectModel>> buildQueue =
new LinkedList<>();
public TestLocalDebuggerModelFactory() {
}
@Override
public CompletableFuture<? extends DebuggerObjectModel> build() {
CompletableFuture<DebuggerObjectModel> future = new CompletableFuture<>();
buildQueue.offer(future);
return future;
}
public CompletableFuture<DebuggerObjectModel> pollBuild() {
return buildQueue.poll();
}
}

View file

@ -15,9 +15,12 @@
*/
package ghidra.util.database;
import javax.help.UnsupportedOperationException;
import ghidra.framework.model.AbortedTransactionListener;
import ghidra.framework.model.UndoableDomainObject;
import ghidra.program.model.data.DataTypeManager;
import ghidra.program.model.listing.ProgramUserData;
import ghidra.util.Msg;
public interface UndoableTransaction extends AutoCloseable {
@ -39,6 +42,11 @@ public interface UndoableTransaction extends AutoCloseable {
return new DataTypeManagerUndoableTransaction(dataTypeManager, tid, commitByDefault);
}
public static UndoableTransaction start(ProgramUserData userData) {
int tid = userData.startTransaction();
return new ProgramUserDataUndoableTransaction(userData, tid);
}
abstract class AbstractUndoableTransaction implements UndoableTransaction {
protected final int transactionID;
@ -109,6 +117,25 @@ public interface UndoableTransaction extends AutoCloseable {
}
}
class ProgramUserDataUndoableTransaction extends AbstractUndoableTransaction {
private final ProgramUserData userData;
private ProgramUserDataUndoableTransaction(ProgramUserData userData, int tid) {
super(tid, true);
this.userData = userData;
}
@Override
public void abort() {
throw new UnsupportedOperationException();
}
@Override
void endTransaction(boolean commit) {
userData.endTransaction(transactionID);
}
}
void commit();
void abort();