mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 02:39:44 +02:00
GP-380: Better quick launch using opinions and offers.
This commit is contained in:
parent
93b2f5cc64
commit
808c20ab7f
28 changed files with 1210 additions and 310 deletions
|
@ -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?
|
||||
|
||||
|
|
|
@ -18,21 +18,19 @@ 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
|
||||
* may change if it proves stable, though, no?
|
||||
*/
|
||||
@FactoryDescription( //
|
||||
brief = "IN-VM MS dbgmodel local debugger", //
|
||||
htmlDetails = "Launch a dbgmodel session in this same JVM" //
|
||||
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() {
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
|
@ -26,31 +25,11 @@ import ghidra.dbg.util.ShellUtils;
|
|||
import ghidra.util.classfinder.ExtensionPointProperties;
|
||||
|
||||
@FactoryDescription( //
|
||||
brief = "GNU gdb local agent via GADP/TCP", //
|
||||
htmlDetails = "Launch a new agent using GDB. This may start a new session or join an existing one." //
|
||||
brief = "GNU gdb local agent via GADP/TCP", //
|
||||
htmlDetails = "Launch a new agent using GDB. This may start a new session or join an existing one." //
|
||||
)
|
||||
@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() {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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)" //
|
||||
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() {
|
||||
|
|
|
@ -2,5 +2,6 @@ AutoReadMemorySpec
|
|||
DebuggerBot
|
||||
DebuggerMappingOpinion
|
||||
DebuggerModelFactory
|
||||
DebuggerProgramLaunchOpinion
|
||||
DisassemblyInject
|
||||
LocationTrackingSpec
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 → 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>
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,8 +61,15 @@ 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 = {
|
||||
DebuggerModelService.class, })
|
||||
@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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,43 +35,82 @@ 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;
|
||||
|
||||
@PluginInfo( //
|
||||
shortDescription = "Debugger models manager service (proxy to front-end)", //
|
||||
description = "Manage debug sessions, connections, and trace recording", //
|
||||
category = PluginCategoryNames.DEBUGGER, //
|
||||
packageName = DebuggerPluginPackage.NAME, //
|
||||
status = PluginStatus.RELEASED, //
|
||||
eventsConsumed = { ProgramActivatedPluginEvent.class, //
|
||||
ProgramClosedPluginEvent.class, //
|
||||
}, //
|
||||
servicesRequired = { //
|
||||
DebuggerTraceManagerService.class, //
|
||||
}, //
|
||||
servicesProvided = { //
|
||||
DebuggerModelService.class, //
|
||||
} //
|
||||
shortDescription = "Debugger models manager service (proxy to front-end)", //
|
||||
description = "Manage debug sessions, connections, and trace recording", //
|
||||
category = PluginCategoryNames.DEBUGGER, //
|
||||
packageName = DebuggerPluginPackage.NAME, //
|
||||
status = PluginStatus.RELEASED, //
|
||||
eventsConsumed = { ProgramActivatedPluginEvent.class, //
|
||||
ProgramClosedPluginEvent.class, //
|
||||
}, //
|
||||
servicesRequired = { //
|
||||
DebuggerTraceManagerService.class, //
|
||||
}, //
|
||||
servicesProvided = { //
|
||||
DebuggerModelService.class, //
|
||||
} //
|
||||
)
|
||||
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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue