From 808c20ab7ffcbe1f9e4646c14a7ed62689a64f33 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Wed, 21 Apr 2021 16:54:30 -0400 Subject: [PATCH] GP-380: Better quick launch using opinions and offers. --- .../DbgEngInJvmDebuggerModelFactory.java | 10 +- .../DbgModelInJvmDebuggerModelFactory.java | 10 +- .../main/java/agent/gdb/GdbCompatibility.java | 51 ++++ .../gdb/GdbInJvmDebuggerModelFactory.java | 9 +- .../gdb/GdbOverSshDebuggerModelFactory.java | 7 +- .../gadp/GdbLocalDebuggerModelFactory.java | 34 +-- ...AbstractGadpLocalDebuggerModelFactory.java | 5 +- .../dbg/jdi/JdiDebuggerModelFactory.java | 10 +- .../Debugger/data/ExtensionPoint.manifest | 1 + .../help/help/topics/Debugger/Debugger.html | 4 +- .../DebuggerModelServicePlugin.html | 26 +- .../core/debug/gui/DebuggerResources.java | 18 +- .../DebuggerMethodInvocationDialog.java | 12 +- .../gui/target/DebuggerConnectDialog.java | 7 +- .../DbgDebuggerProgramLaunchOpinion.java | 167 +++++++++++ .../GdbDebuggerProgramLaunchOpinion.java | 144 +++++++++ .../model/DebuggerModelServicePlugin.java | 46 ++- .../DebuggerModelServiceProxyPlugin.java | 263 ++++++++++++----- .../AbstractDebuggerProgramLaunchOffer.java | 279 ++++++++++++++++++ .../launch/DebuggerProgramLaunchOffer.java | 109 +++++++ .../launch/DebuggerProgramLaunchOpinion.java | 28 ++ .../app/services/DebuggerModelService.java | 49 ++- .../model/DebuggerModelServiceTest.java | 33 +-- .../TestDebuggerProgramLaunchOpinion.java | 67 +++++ .../java/ghidra/dbg/DebuggerObjectModel.java | 16 - .../ghidra/dbg/LocalDebuggerModelFactory.java | 44 --- .../model/TestLocalDebuggerModelFactory.java | 44 --- .../util/database/UndoableTransaction.java | 27 ++ 28 files changed, 1210 insertions(+), 310 deletions(-) create mode 100644 Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbCompatibility.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgDebuggerProgramLaunchOpinion.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbDebuggerProgramLaunchOpinion.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOpinion.java create mode 100644 Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestDebuggerProgramLaunchOpinion.java delete mode 100644 Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/LocalDebuggerModelFactory.java delete mode 100644 Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestLocalDebuggerModelFactory.java diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/DbgEngInJvmDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/DbgEngInJvmDebuggerModelFactory.java index 676275268b..a01b4b4334 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/DbgEngInJvmDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/DbgEngInJvmDebuggerModelFactory.java @@ -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? diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/DbgModelInJvmDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/DbgModelInJvmDebuggerModelFactory.java index 2c2b22be0c..0cee6215b9 100644 --- a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/DbgModelInJvmDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/DbgModelInJvmDebuggerModelFactory.java @@ -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 build() { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbCompatibility.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbCompatibility.java new file mode 100644 index 0000000000..8570e20d61 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbCompatibility.java @@ -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 cache = new HashMap<>(); + + public boolean isCompatible(String gdbCmd) { + List args = ShellUtils.parseArgs(gdbCmd); + if (args.isEmpty()) { + return false; + } + return cache.computeIfAbsent(gdbCmd, p -> checkGdbPresent(args.get(0))); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java index e13ca8de2d..e3fcde49a1 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java @@ -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() { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java index 8c990af2fc..df2abe239b 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java @@ -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") diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/GdbLocalDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/GdbLocalDebuggerModelFactory.java index 34f2d4be01..5aec358bb6 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/GdbLocalDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/GdbLocalDebuggerModelFactory.java @@ -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 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 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() { diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpLocalDebuggerModelFactory.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpLocalDebuggerModelFactory.java index 79e2dcbabe..bfbbbe4928 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpLocalDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpLocalDebuggerModelFactory.java @@ -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"; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/JdiDebuggerModelFactory.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/JdiDebuggerModelFactory.java index fe13d67a0c..bbcdcc5aba 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/JdiDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/JdiDebuggerModelFactory.java @@ -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 build() { diff --git a/Ghidra/Debug/Debugger/data/ExtensionPoint.manifest b/Ghidra/Debug/Debugger/data/ExtensionPoint.manifest index 7c0a051073..bcbfbe4845 100644 --- a/Ghidra/Debug/Debugger/data/ExtensionPoint.manifest +++ b/Ghidra/Debug/Debugger/data/ExtensionPoint.manifest @@ -2,5 +2,6 @@ AutoReadMemorySpec DebuggerBot DebuggerMappingOpinion DebuggerModelFactory +DebuggerProgramLaunchOpinion DisassemblyInject LocationTrackingSpec diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/Debugger/Debugger.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/Debugger/Debugger.html index 60c567bbe7..2136146bb1 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/Debugger/Debugger.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/Debugger/Debugger.html @@ -50,8 +50,8 @@
  • 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.
  • + present," i.e., corresponds to a live target machine state. They may directly command and/or + request additional information from the target.
  • Global manipulation - those which aggregate information from several targets or traces, presenting a comprehensive picture. Modifications in these views may be directed to any diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModelServicePlugin/DebuggerModelServicePlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModelServicePlugin/DebuggerModelServicePlugin.html index 31db45d57f..e460687ad7 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModelServicePlugin/DebuggerModelServicePlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModelServicePlugin/DebuggerModelServicePlugin.html @@ -24,15 +24,31 @@

    Debug Program

    -

    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 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 Record Automatically is enabled, this will provide a one-click action to debug the current program. This is similar to the Quick Launch - action in the Commands and Objects window, except this one creates a new connection.

    + action in the Commands and Objects window, except this one does not require an existing + connection.

    + +

    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 Debugger → Debug Program 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.

    Disconnect All

    diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java index 18eb37cc9a..70b07207b1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java @@ -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 MultiStateActionBuilder buttonBuilder(Plugin owner, Plugin helpOwner) { + return new MultiStateActionBuilder(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)); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java index 0bddda8f81..fc3e46e454 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java @@ -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> parameters; // TODO: Not sure this is the best keying, but I think it works. private Map memorized = new HashMap<>(); @@ -115,14 +114,14 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider ntp -> parameter.defaultValue); } - public Map promptArguments(TargetParameterMap parameterMap) { + public Map promptArguments(Map> parameterMap) { setParameters(parameterMap); tool.showDialog(this); return getArguments(); } - public void setParameters(TargetParameterMap parameterMap) { + public void setParameters(Map> 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 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()); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java index 6243c06a57..b631b30bc5 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java @@ -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); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgDebuggerProgramLaunchOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgDebuggerProgramLaunchOpinion.java new file mode 100644 index 0000000000..1ebd864a9f --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgDebuggerProgramLaunchOpinion.java @@ -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 getLauncherPath() { + return PathUtils.parse(""); + } + + @Override + protected Map generateDefaultLauncherArgs( + Map> 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 getOffers(Program program, PluginTool tool, + DebuggerModelService service) { + String exe = program.getExecutablePath(); + if (exe == null || "".equals(exe.trim())) { + return List.of(); + } + List 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; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbDebuggerProgramLaunchOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbDebuggerProgramLaunchOpinion.java new file mode 100644 index 0000000000..f4dc976503 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbDebuggerProgramLaunchOpinion.java @@ -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 getLauncherPath() { + return PathUtils.parse("Inferiors[1]"); + } + + @Override + protected Map generateDefaultLauncherArgs( + Map> 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> 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 getOffers(Program program, PluginTool tool, + DebuggerModelService service) { + String exe = program.getExecutablePath(); + if (exe == null || "".equals(exe.trim())) { + return List.of(); + } + List 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; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java index 2150f8f613..7c5ceef1d9 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java @@ -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 startLocalSession() { - LocalDebuggerModelFactory factory = getDefaultLocalDebuggerModelFactory(); - if (factory == null) { - return CompletableFuture.failedFuture( - new NoSuchElementException("No suitable launcher for the local platform")); - } - CompletableFuture 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 getProgramLaunchOffers(Program program) { + return ClassSearcher.getInstances(DebuggerProgramLaunchOpinion.class) + .stream() + .flatMap(opinion -> opinion.getOffers(program, tool, this).stream()); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java index 26e8f222ff..da631181eb 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java @@ -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 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 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 actionDebugProgram; + Set actionDebugProgramMenus = new HashSet<>(); DockingAction actionDisconnectAll; protected final ListenerSet> 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 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 debugProgram(Program __, TaskMonitor monitor) { - monitor.initialize(3); - monitor.setMessage("Starting local session"); - return startLocalSession().thenCompose(model -> { - CompletableFuture 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 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 getProgramLaunchOffers(Program program) { + return orderOffers(delegate.getProgramLaunchOffers(program), program); + } + + protected List 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 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 { + Map fastIndex = new HashMap<>(); + + public OfferComparator(List 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 orderOffers( + Stream offers, Program program) { + List 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 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 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 offers = program == null ? List.of() + : getProgramLaunchOffers(program).collect(Collectors.toList()); + List> states = offers.stream() + .map(o -> new ActionState(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 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 startLocalSession() { - return delegate.startLocalSession(); - } - @Override public TraceRecorder recordTarget(TargetObject target, DebuggerTargetTraceMapper mapper) throws IOException { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java new file mode 100644 index 0000000000..a5de54c911 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java @@ -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 getLauncherPath() { + return PathUtils.parse(""); + } + + private void saveLauncherArgs(Map args, + Map> 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 takeDefaultsForParameters( + Map> params) { + return params.values().stream().collect(Collectors.toMap(p -> p.name, p -> p.defaultValue)); + } + + /** + * Generate the default launcher arguments + * + *

    + * 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 generateDefaultLauncherArgs( + Map> 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 promptLauncherArgs(Map> params) { + DebuggerMethodInvocationDialog dialog = + new DebuggerMethodInvocationDialog(tool, getButtonTitle(), "Launch", getIcon()); + // NB. Do not invoke read/writeConfigState + Map 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 + * + *

    + * 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 loadLastLauncherArgs( + Map> 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 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 args = generateDefaultLauncherArgs(params); + saveLauncherArgs(args, params); + return args; + } + + /** + * Obtain the launcher args + * + *

    + * 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 getLauncherArgs(Map> 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 implements DebuggerModelListener { + private final DebuggerObjectModel model; + private final List path; + + public ValueExpecter(DebuggerObjectModel model, List 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 removed, + Map added) { + retryFetch(); + } + + @Override + public void elementsChanged(TargetObject object, Collection removed, + Map added) { + retryFetch(); + } + } + + @Override + public CompletableFuture 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 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); + }); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java new file mode 100644 index 0000000000..f4d6996bc2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java @@ -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 + * + *

    + * 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 launchProgram(TaskMonitor monitor, boolean prompt); + + /** + * A name so that this offer can be recognized later + * + *

    + * 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 cannot 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 + * + *

    + * 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 + * + *

    + * 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 + * + *

    + * 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; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOpinion.java new file mode 100644 index 0000000000..6f686afa4d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOpinion.java @@ -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 getOffers(Program program, PluginTool tool, + DebuggerModelService service); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java index d0d3233653..8015b9c27f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java @@ -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 * + *

    * 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 * + *

    * 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 startLocalSession(); - /** * Start a new trace on the given target * + *

    * 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. * + *

    * Any given container can be traced by at most one recorder. * + *

    * 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 * + *

    * 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 * + *

    * 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}. * + *

    * 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 * + *

    * 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 * + *

    * A recorder uses conventions to discover the "process" in the model, given a target object. * + *

    * 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 * + *

    * 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." * + *

    * 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 * + *

    * 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 * + *

    * 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 * + *

    * 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 * + *

    * 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 * + *

    * The caller must beep a strong reference to the listener, or it will be automatically removed. * + *

    * 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 * + *

    * 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 * + *

    * The caller must beep a strong reference to the listener, or it will be automatically removed. * + *

    * 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 * + *

    * TODO: Probably replace this with a {@link PluginEvent} * * @param listener the listener */ void removeTraceRecordersChangedListener(CollectionChangeListener listener); + + /** + * Collect all offers for launching the given program + * + * @param program the program to launch + * @return the offers + */ + Stream getProgramLaunchOffers(Program program); } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java index 1889c3b723..b1ef828057 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java @@ -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 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 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(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestDebuggerProgramLaunchOpinion.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestDebuggerProgramLaunchOpinion.java new file mode 100644 index 0000000000..27b6baab6a --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/TestDebuggerProgramLaunchOpinion.java @@ -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 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 getOffers(Program program, PluginTool tool, + DebuggerModelService service) { + DebuggerModelFactory factory = Unique.assertOne(service.getModelFactories()); + assertEquals(TestDebuggerModelFactory.class, factory.getClass()); + + return List.of(new TestDebuggerProgramLaunchOffer()); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java index b8806d47b5..2f4a665df2 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java @@ -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 diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/LocalDebuggerModelFactory.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/LocalDebuggerModelFactory.java deleted file mode 100644 index a3aa178858..0000000000 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/LocalDebuggerModelFactory.java +++ /dev/null @@ -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 - * - *

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

    - * In the event multiple compatible factories are discovered, the one with the highest priority - * is selected, breaking ties arbitrarily. - * - *

    - * 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()); - } -} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestLocalDebuggerModelFactory.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestLocalDebuggerModelFactory.java deleted file mode 100644 index 1beea17a2f..0000000000 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestLocalDebuggerModelFactory.java +++ /dev/null @@ -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> buildQueue = - new LinkedList<>(); - - public TestLocalDebuggerModelFactory() { - } - - @Override - public CompletableFuture build() { - CompletableFuture future = new CompletableFuture<>(); - buildQueue.offer(future); - return future; - } - - public CompletableFuture pollBuild() { - return buildQueue.poll(); - } -} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/UndoableTransaction.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/UndoableTransaction.java index d3d29a2305..9ba290dfb7 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/UndoableTransaction.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/UndoableTransaction.java @@ -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();