GP-2623: Improve connect dialog and factory descriptions

This commit is contained in:
Dan 2023-01-18 16:53:01 -05:00
parent 5195aaebc1
commit 8dbf2341b2
30 changed files with 565 additions and 333 deletions

View file

@ -23,15 +23,16 @@ import agent.dbgeng.model.impl.DbgModelImpl;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.program.model.listing.Program;
/**
* 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" //
)
@FactoryDescription(
brief = "MS dbgeng.dll (WinDbg)",
htmlDetails = """
Connect to the Microsoft Debug Engine.
This is the same engine that powers WinDbg.
This is best for most Windows userspace and kernel targets.
Kernel debugging is still experimental.
This will access the native API, which may put Ghidra's JVM at risk.""")
public class DbgEngInJvmDebuggerModelFactory implements DebuggerModelFactory {
protected String remote = "none"; // Require user to start server
@ -53,8 +54,18 @@ public class DbgEngInJvmDebuggerModelFactory implements DebuggerModelFactory {
}
@Override
public boolean isCompatible() {
return System.getProperty("os.name").toLowerCase().contains("windows");
public int getPriority(Program program) {
// TODO: Might instead look for the DLL
if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
return -1;
}
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 80;
}
public String getAgentTransport() {

View file

@ -19,14 +19,17 @@ import java.util.List;
import ghidra.dbg.gadp.server.AbstractGadpLocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
import ghidra.program.model.listing.Program;
@FactoryDescription( //
brief = "MS dbgeng.dll (WinDbg) local agent via GADP/TCP", //
htmlDetails = "Launch a new agent using the Microsoft Debug Engine." //
)
@ExtensionPointProperties(priority = 100)
public class DbgEngLocalDebuggerModelFactory extends AbstractGadpLocalDebuggerModelFactory {
@FactoryDescription(
brief = "MS dbgeng.dll (WinDbg) via GADP",
htmlDetails = """
Connect to the Microsoft Debug Engine.
This is the same engine that powers WinDbg.
This is best for most Windows userspace and kernel targets.
Kernel debugging is still experimental.
This will protect Ghidra's JVM by using a subprocess to access the native API.""")
public class DbgEngGadpDebuggerModelFactory extends AbstractGadpLocalDebuggerModelFactory {
protected String remote = "none"; // Require user to start server
@FactoryOption("DebugConnect options (.server)")
@ -39,9 +42,18 @@ public class DbgEngLocalDebuggerModelFactory extends AbstractGadpLocalDebuggerMo
Property.fromAccessors(String.class, this::getAgentTransport, this::setAgentTransport);
@Override
public boolean isCompatible() {
public int getPriority(Program program) {
// TODO: Might instead look for the DLL
return System.getProperty("os.name").toLowerCase().contains("windows");
if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
return -1;
}
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 60;
}
public String getAgentTransport() {

View file

@ -16,7 +16,7 @@
package agent.dbgeng.model.gadp;
import agent.dbgeng.dbgeng.DbgEngTest;
import agent.dbgeng.gadp.DbgEngLocalDebuggerModelFactory;
import agent.dbgeng.gadp.DbgEngGadpDebuggerModelFactory;
import agent.dbgeng.model.AbstractDbgengModelHost;
import ghidra.dbg.DebuggerModelFactory;
@ -24,6 +24,6 @@ public class GadpDbgengModelHost extends AbstractDbgengModelHost {
@Override
public DebuggerModelFactory getModelFactory() {
DbgEngTest.assumeDbgengDLLLoadable();
return new DbgEngLocalDebuggerModelFactory();
return new DbgEngGadpDebuggerModelFactory();
}
}

View file

@ -23,15 +23,14 @@ import agent.dbgmodel.model.impl.DbgModel2Impl;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.program.model.listing.Program;
/**
* 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" //
)
@FactoryDescription(
brief = "MS dbgmodel.dll (WinDbg Preview)",
htmlDetails = """
Connect to the Microsoft Debug Model.
This is the same engine that powers WinDbg 2.
This will access the native API, which may put Ghidra's JVM at risk.""")
public class DbgModelInJvmDebuggerModelFactory implements DebuggerModelFactory {
protected String remote = "none"; // Require user to start server
@ -53,8 +52,18 @@ public class DbgModelInJvmDebuggerModelFactory implements DebuggerModelFactory {
}
@Override
public boolean isCompatible() {
return System.getProperty("os.name").toLowerCase().contains("windows");
public int getPriority(Program program) {
// TODO: Might instead look for the DLL
if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
return -1;
}
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 70;
}
public String getAgentTransport() {

View file

@ -15,16 +15,17 @@
*/
package agent.dbgmodel.gadp;
import agent.dbgeng.gadp.DbgEngLocalDebuggerModelFactory;
import agent.dbgeng.gadp.DbgEngGadpDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
import ghidra.program.model.listing.Program;
@FactoryDescription( //
brief = "MS dbgmodel.dll (WinDbg 2) local agent via GADP/TCP", //
htmlDetails = "Launch a new agent using the Microsoft Debug Model (best for WinDbg 2)." //
)
@ExtensionPointProperties(priority = 90)
public class DbgModelLocalDebuggerModelFactory extends DbgEngLocalDebuggerModelFactory {
@FactoryDescription(
brief = "MS dbgmodel.dll (WinDbg Preview) via GADP/TCP",
htmlDetails = """
Connect to the Microsoft Debug Model.
This is the same engine that powers WinDbg 2.
This will protect Ghidra's JVM by using a subprocess to access the native API.""")
public class DbgModelGadpDebuggerModelFactory extends DbgEngGadpDebuggerModelFactory {
@Override
protected String getThreadName() {
@ -35,4 +36,19 @@ public class DbgModelLocalDebuggerModelFactory extends DbgEngLocalDebuggerModelF
protected Class<?> getServerClass() {
return DbgModelGadpServer.class;
}
@Override
public int getPriority(Program program) {
// TODO: Might instead look for the DLL
if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
return -1;
}
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 50;
}
}

View file

@ -21,17 +21,14 @@ import agent.frida.model.impl.FridaModelImpl;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
import ghidra.program.model.listing.Program;
/**
* 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 Frida local debugger", //
htmlDetails = "Launch a Frida session in this same JVM" //
)
@ExtensionPointProperties(priority = 80)
@FactoryDescription(
brief = "PROTOTYPE: Frida",
htmlDetails = """
Connect to Frida.
This is an experimental connector. Use at your own risk.
This will access the native API, which may put Ghidra's JVM at risk.""")
public class FridaInJvmDebuggerModelFactory implements DebuggerModelFactory {
@Override
@ -41,9 +38,18 @@ public class FridaInJvmDebuggerModelFactory implements DebuggerModelFactory {
}
@Override
public boolean isCompatible() {
String osname = System.getProperty("os.name");
return osname.contains("Mac OS X") || osname.contains("Linux") || osname.contains("Windows");
public int getPriority(Program program) {
String osname = System.getProperty("os.name").toLowerCase();
if (!(osname.contains("mac os x") || osname.contains("linux") ||
osname.contains("windows"))) {
return -1;
}
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 30;
}
}

View file

@ -0,0 +1,63 @@
/* ###
* 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.frida.gadp;
import java.util.List;
import ghidra.dbg.gadp.server.AbstractGadpLocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.program.model.listing.Program;
@FactoryDescription(
brief = "PROTOTYPE: Frida via GADP",
htmlDetails = """
Connect to Frida.
This is an experimental connector. Use at your own risk.
This will protect Ghidra's JVM by using a subprocess to access the native API.""")
public class FridaGadpDebuggerModelFactory extends AbstractGadpLocalDebuggerModelFactory {
@Override
public int getPriority(Program program) {
String osname = System.getProperty("os.name").toLowerCase();
if (!(osname.contains("mac os x") || osname.contains("linux") ||
osname.contains("windows"))) {
return -1;
}
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 25;
}
@Override
protected String getThreadName() {
return "Local Frida Agent stdout";
}
protected Class<?> getServerClass() {
return FridaGadpServer.class;
}
@Override
protected void completeCommandLine(List<String> cmd) {
cmd.add(getServerClass().getCanonicalName());
cmd.addAll(List.of("-H", host));
cmd.addAll(List.of("-p", Integer.toString(port)));
}
}

View file

@ -1,78 +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 agent.frida.gadp;
import java.util.List;
import ghidra.dbg.gadp.server.AbstractGadpLocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
@FactoryDescription( //
brief = "Frida local agent via GADP/TCP", //
htmlDetails = "Launch a new agent using Frida." //
)
@ExtensionPointProperties(priority = 100)
public class FridaLocalDebuggerModelFactory extends AbstractGadpLocalDebuggerModelFactory {
protected String remote = "none"; // Require user to start server
@FactoryOption("DebugConnect options (.server)")
public final Property<String> agentRemoteOption =
Property.fromAccessors(String.class, this::getAgentRemote, this::setAgentRemote);
protected String transport = "none"; // Require user to start server
@FactoryOption("Remote process server options (untested)")
public final Property<String> agentTransportOption =
Property.fromAccessors(String.class, this::getAgentTransport, this::setAgentTransport);
@Override
public boolean isCompatible() {
String osname = System.getProperty("os.name");
return osname.contains("Mac OS X") || osname.contains("Linux") || osname.contains("Windows");
}
public String getAgentTransport() {
return transport;
}
public void setAgentTransport(String transport) {
this.transport = transport;
}
public String getAgentRemote() {
return remote;
}
public void setAgentRemote(String remote) {
this.remote = remote;
}
@Override
protected String getThreadName() {
return "Local Frida Agent stdout";
}
protected Class<?> getServerClass() {
return FridaGadpServer.class;
}
@Override
protected void completeCommandLine(List<String> cmd) {
cmd.add(getServerClass().getCanonicalName());
cmd.addAll(List.of("-H", host));
cmd.addAll(List.of("-p", Integer.toString(port)));
}
}

View file

@ -17,7 +17,7 @@ package agent.frida.model.gadp;
import static org.junit.Assume.assumeFalse;
import agent.frida.gadp.FridaLocalDebuggerModelFactory;
import agent.frida.gadp.FridaGadpDebuggerModelFactory;
import agent.frida.model.AbstractFridaModelHost;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.util.SystemUtilities;
@ -26,6 +26,6 @@ class GadpFridaModelHost extends AbstractFridaModelHost {
@Override
public DebuggerModelFactory getModelFactory() {
assumeFalse("Not ready for CI", SystemUtilities.isInTestingBatchMode());
return new FridaLocalDebuggerModelFactory();
return new FridaGadpDebuggerModelFactory();
}
}

View file

@ -25,15 +25,16 @@ import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.dbg.util.ShellUtils;
import ghidra.program.model.listing.Program;
/**
* 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 GNU gdb local debugger", //
htmlDetails = "Launch a GDB session in this same JVM" //
)
@FactoryDescription(
brief = "gdb",
htmlDetails = """
Connect to gdb.
This is best for most Linux and Unix userspace targets, and many embedded targets.
It may also be used with gdbserver by connecting to gdb, then using <code>target remote
...</code>.
This will access the native API, which may put Ghidra's JVM at risk.""")
public class GdbInJvmDebuggerModelFactory implements DebuggerModelFactory {
private String gdbCmd = GdbManager.DEFAULT_GDB_CMD;
@ -59,8 +60,17 @@ public class GdbInJvmDebuggerModelFactory implements DebuggerModelFactory {
}
@Override
public boolean isCompatible() {
return GdbCompatibility.INSTANCE.isCompatible(gdbCmd);
public int getPriority(Program program) {
if (!GdbCompatibility.INSTANCE.isCompatible(gdbCmd)) {
return -1;
}
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 80;
}
public String getGdbCommand() {

View file

@ -24,10 +24,14 @@ import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ShellUtils;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.program.model.listing.Program;
@FactoryDescription(
brief = "GNU gdb via SSH",
htmlDetails = "Launch a GDB session over an SSH connection")
brief = "gdb via SSH",
htmlDetails = """
Connect to gdb using SSH.
This is best for remote Linux and Unix userspace targets when gdb is installed on the
remote host.""")
public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory {
private String gdbCmd = "/usr/bin/gdb";
@ -91,8 +95,14 @@ public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory {
}
@Override
public boolean isCompatible() {
return true;
public int getPriority(Program program) {
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 75;
}
public String getGdbCommand() {

View file

@ -22,14 +22,17 @@ import agent.gdb.manager.GdbManager;
import ghidra.dbg.gadp.server.AbstractGadpLocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.dbg.util.ShellUtils;
import ghidra.util.classfinder.ExtensionPointProperties;
import ghidra.program.model.listing.Program;
@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." //
)
@ExtensionPointProperties(priority = 100)
public class GdbLocalDebuggerModelFactory extends AbstractGadpLocalDebuggerModelFactory {
@FactoryDescription(
brief = "gdb via GADP",
htmlDetails = """
Connect to gdb.
This is best for most Linux and Unix userspace targets, and many embedded targets.
This will protect Ghidra's JVM by using a subprocess to access the native API.
If you are using <b>gdbserver</b>, you must connect to gdb first (consider the non-GADP
connector), then use <code>target remote ...</code> to connect to your target.""")
public class GdbGadpDebuggerModelFactory extends AbstractGadpLocalDebuggerModelFactory {
private String gdbCmd = GdbManager.DEFAULT_GDB_CMD;
@FactoryOption("GDB launch command")
@ -44,9 +47,17 @@ public class GdbLocalDebuggerModelFactory extends AbstractGadpLocalDebuggerModel
// TODO: newLine option?
@Override
public boolean isCompatible() {
// TODO: Could potentially support GDB on Windows, but the pty thing would need porting.
return GdbCompatibility.INSTANCE.isCompatible(gdbCmd);
public int getPriority(Program program) {
if (!GdbCompatibility.INSTANCE.isCompatible(gdbCmd)) {
return -1;
}
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 60;
}
public String getGdbCommand() {

View file

@ -20,7 +20,7 @@ import static org.junit.Assume.assumeTrue;
import java.io.File;
import agent.gdb.gadp.GdbLocalDebuggerModelFactory;
import agent.gdb.gadp.GdbGadpDebuggerModelFactory;
import agent.gdb.model.AbstractGdbModelHost;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.util.SystemUtilities;
@ -30,6 +30,6 @@ class GadpGdbModelHost extends AbstractGdbModelHost {
public DebuggerModelFactory getModelFactory() {
assumeFalse("Not ready for CI", SystemUtilities.isInTestingBatchMode());
assumeTrue("GDB cannot be found", new File("/usr/bin/gdb").canExecute());
return new GdbLocalDebuggerModelFactory();
return new GdbGadpDebuggerModelFactory();
}
}

View file

@ -21,21 +21,16 @@ import agent.lldb.model.impl.LldbModelImpl;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
import ghidra.program.model.listing.Program;
/**
* 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 lldb local debugger", //
htmlDetails = "Launch a lldb session in this same JVM" //
)
@ExtensionPointProperties(priority = 80)
@FactoryDescription(
brief = "lldb",
htmlDetails = """
Connect to lldb.
This is best for most macOS and iOS targets, but supports many others.
This will access the native API, which may put Ghidra's JVM at risk.""")
public class LldbInJvmDebuggerModelFactory implements DebuggerModelFactory {
// TODO remoteTransport option?
@Override
public CompletableFuture<? extends DebuggerObjectModel> build() {
LldbModelImpl model = new LldbModelImpl();
@ -43,9 +38,19 @@ public class LldbInJvmDebuggerModelFactory implements DebuggerModelFactory {
}
@Override
public boolean isCompatible() {
String osname = System.getProperty("os.name");
return osname.contains("Mac OS X") || osname.contains("Linux") || osname.contains("Windows");
public int getPriority(Program program) {
String osname = System.getProperty("os.name").toLowerCase();
if (!(osname.contains("mac os x") || osname.contains("linux") ||
osname.contains("windows"))) {
return -1;
}
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 40;
}
}

View file

@ -0,0 +1,63 @@
/* ###
* 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.lldb.gadp;
import java.util.List;
import ghidra.dbg.gadp.server.AbstractGadpLocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.program.model.listing.Program;
@FactoryDescription(
brief = "lldb via GADP",
htmlDetails = """
Connect to lldb.
This is best for most macOS and iOS targets, but supports many others.
This will protect Ghidra's JVM by using a subprocess to access the native API.""")
public class LldbGadpDebuggerModelFactory extends AbstractGadpLocalDebuggerModelFactory {
@Override
public int getPriority(Program program) {
String osname = System.getProperty("os.name").toLowerCase();
if (!(osname.contains("mac os x") || osname.contains("linux") ||
osname.contains("windows"))) {
return -1;
}
if (program != null) {
String exe = program.getExecutablePath();
if (exe == null || exe.isBlank()) {
return -1;
}
}
return 35;
}
@Override
protected String getThreadName() {
return "Local LLDB Agent stdout";
}
protected Class<?> getServerClass() {
return LldbGadpServer.class;
}
@Override
protected void completeCommandLine(List<String> cmd) {
cmd.add(getServerClass().getCanonicalName());
cmd.addAll(List.of("-H", host));
cmd.addAll(List.of("-p", Integer.toString(port)));
}
}

View file

@ -1,78 +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 agent.lldb.gadp;
import java.util.List;
import ghidra.dbg.gadp.server.AbstractGadpLocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.util.classfinder.ExtensionPointProperties;
@FactoryDescription( //
brief = "LLVM lldb local agent via GADP/TCP", //
htmlDetails = "Launch a new agent using LLVM's lldb." //
)
@ExtensionPointProperties(priority = 100)
public class LldbLocalDebuggerModelFactory extends AbstractGadpLocalDebuggerModelFactory {
protected String remote = "none"; // Require user to start server
@FactoryOption("DebugConnect options (.server)")
public final Property<String> agentRemoteOption =
Property.fromAccessors(String.class, this::getAgentRemote, this::setAgentRemote);
protected String transport = "none"; // Require user to start server
@FactoryOption("Remote process server options (untested)")
public final Property<String> agentTransportOption =
Property.fromAccessors(String.class, this::getAgentTransport, this::setAgentTransport);
@Override
public boolean isCompatible() {
String osname = System.getProperty("os.name");
return osname.contains("Mac OS X") || osname.contains("Linux") || osname.contains("Windows");
}
public String getAgentTransport() {
return transport;
}
public void setAgentTransport(String transport) {
this.transport = transport;
}
public String getAgentRemote() {
return remote;
}
public void setAgentRemote(String remote) {
this.remote = remote;
}
@Override
protected String getThreadName() {
return "Local LLDB Agent stdout";
}
protected Class<?> getServerClass() {
return LldbGadpServer.class;
}
@Override
protected void completeCommandLine(List<String> cmd) {
cmd.add(getServerClass().getCanonicalName());
cmd.addAll(List.of("-H", host));
cmd.addAll(List.of("-p", Integer.toString(port)));
}
}

View file

@ -26,10 +26,12 @@ import ghidra.async.TypeSpec;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
@FactoryDescription( //
brief = "GADP connection over TCP", //
htmlDetails = "Connect to an optionally remote agent via GADP/TCP." //
)
@FactoryDescription(
brief = "Ghidra debug agent (GADP)",
htmlDetails = """
Connect to a Ghidra debug agent using GADP.
This is Ghidra's debugging protocol.
Use this if launching a (usually remote) agent manually.""")
public class GadpTcpDebuggerModelFactory implements DebuggerModelFactory {
private String host = "localhost";

View file

@ -22,10 +22,12 @@ import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.jdi.model.JdiModelImpl;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
@FactoryDescription( //
brief = "JDI debugger", //
htmlDetails = "Debug a Java or Dalvik VM (supports JDWP)" //
)
@FactoryDescription(
brief = "PROTOTYPE: JDWP (Java or Dalvik)",
htmlDetails = """
Connect to a Java or Dalvik VM via JDWP.
This is the same debugging protocol used by most Java IDEs.
Support for debugging Java and Dalvik is still experimental.""")
public class JdiDebuggerModelFactory implements DebuggerModelFactory {
@Override

View file

@ -34,10 +34,12 @@ import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import ghidra.app.services.DebuggerModelService;
import ghidra.app.services.ProgramManager;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.program.model.listing.Program;
public class DebuggerTargetsProvider extends ComponentProviderAdapter {
@ -107,7 +109,9 @@ public class DebuggerTargetsProvider extends ComponentProviderAdapter {
public void actionPerformed(ActionContext context) {
// NB. Drop the future on the floor, because the UI will report issues.
// Cancellation should be ignored.
modelService.showConnectDialog();
ProgramManager programManager = tool.getService(ProgramManager.class);
Program program = programManager == null ? null : programManager.getCurrentProgram();
modelService.showConnectDialog(program);
}
@Override

View file

@ -37,7 +37,6 @@ public class DbgDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpi
protected List<String> getLauncherPath() {
return PathUtils.parse("");
}
}
protected class InVmDbgengDebuggerProgramLaunchOffer
@ -133,7 +132,7 @@ public class DbgDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpi
}
List<DebuggerProgramLaunchOffer> offers = new ArrayList<>();
for (DebuggerModelFactory factory : service.getModelFactories()) {
if (!factory.isCompatible()) {
if (!factory.isCompatible(program)) {
continue;
}
String clsName = factory.getClass().getName();

View file

@ -95,7 +95,7 @@ public class FridaDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchO
}
List<DebuggerProgramLaunchOffer> offers = new ArrayList<>();
for (DebuggerModelFactory factory : service.getModelFactories()) {
if (!factory.isCompatible()) {
if (!factory.isCompatible(program)) {
continue;
}
String clsName = factory.getClass().getName();

View file

@ -139,7 +139,7 @@ public class GdbDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpi
}
List<DebuggerProgramLaunchOffer> offers = new ArrayList<>();
for (DebuggerModelFactory factory : service.getModelFactories()) {
if (!factory.isCompatible()) {
if (!factory.isCompatible(program)) {
continue;
}
String clsName = factory.getClass().getName();

View file

@ -90,7 +90,7 @@ public class LldbDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOp
}
List<DebuggerProgramLaunchOffer> offers = new ArrayList<>();
for (DebuggerModelFactory factory : service.getModelFactories()) {
if (!factory.isCompatible()) {
if (!factory.isCompatible(program)) {
continue;
}
String clsName = factory.getClass().getName();

View file

@ -26,6 +26,7 @@ import java.util.concurrent.CompletableFuture;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.text.View;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
@ -40,13 +41,14 @@ import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ConfigurableFactory.Property;
import ghidra.framework.options.SaveState;
import ghidra.program.model.listing.Program;
import ghidra.util.*;
import ghidra.util.datastruct.CollectionChangeListener;
import ghidra.util.layout.PairLayout;
public class DebuggerConnectDialog extends DialogComponentProvider
implements PropertyChangeListener {
private static final String KEY_FACTORY_CLASSNAME = "factoryClassname";
private static final String KEY_CURRENT_FACTORY_CLASSNAME = "currentFactoryCls";
private static final String KEY_SUCCESS_FACTORY_CLASSNAME = "successFactoryCls";
private static final String HTML_BOLD_DESCRIPTION = "<html><b>Description:</b> ";
protected class FactoriesChangedListener
@ -67,9 +69,40 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
}
protected record FactoryEntry(DebuggerModelFactory factory) {
@Override
public String toString() {
return factory.getBrief();
}
}
protected record PrioritizedFactory(FactoryEntry entry, int priority) {
public PrioritizedFactory(FactoryEntry ent, Program program) {
this(ent, ent.factory.getPriority(program));
}
}
protected enum NameComparator implements Comparator<String> {
INSTANCE;
@Override
public int compare(String o1, String o2) {
boolean p1 = o1.startsWith("PROTOTYPE:");
boolean p2 = o2.startsWith("PROTOTYPE:");
if (p1 && !p2) {
return 1;
}
if (!p1 && p2) {
return -1;
}
return o1.toLowerCase().compareTo(o2.toLowerCase());
}
}
private DebuggerModelService modelService;
private DebuggerModelFactory factory;
private DebuggerModelFactory currentFactory;
private DebuggerModelFactory successFactory;
private final Map<DebuggerModelFactory, FactoryEntry> factories = new HashMap<>();
private FactoriesChangedListener listener = new FactoriesChangedListener();
@ -81,26 +114,12 @@ public class DebuggerConnectDialog extends DialogComponentProvider
private final Map<Property<?>, Component> components = new LinkedHashMap<>();
protected JLabel description;
protected JPanel pairPanel;
private PairLayout layout;
protected JPanel gridPanel;
protected JButton connectButton;
protected CompletableFuture<? extends DebuggerObjectModel> futureConnect;
protected CompletableFuture<DebuggerObjectModel> result;
protected static class FactoryEntry {
DebuggerModelFactory factory;
public FactoryEntry(DebuggerModelFactory factory) {
this.factory = factory;
}
@Override
public String toString() {
return factory.getBrief();
}
}
public DebuggerConnectDialog() {
super(AbstractConnectAction.NAME, true, true, true, false);
@ -126,7 +145,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
toAdd.add(entry);
}
SwingUtilities.invokeLater(() -> {
toAdd.sort(Comparator.comparing(FactoryEntry::toString));
toAdd.sort(Comparator.comparing(FactoryEntry::toString, NameComparator.INSTANCE));
for (FactoryEntry entry : toAdd) {
dropdownModel.addElement(entry);
}
@ -190,20 +209,20 @@ public class DebuggerConnectDialog extends DialogComponentProvider
JPanel inner = new JPanel(new BorderLayout());
description = new JLabel(HTML_BOLD_DESCRIPTION + "</html>");
description.setBorder(new EmptyBorder(10, 0, 10, 0));
description.setPreferredSize(new Dimension(400, 150));
inner.add(description);
topBox.add(inner);
panel.add(topBox, BorderLayout.NORTH);
layout = new PairLayout(5, 5);
pairPanel = new JPanel(layout);
gridPanel = new JPanel(new GridBagLayout());
JPanel centering = new JPanel(new FlowLayout(FlowLayout.CENTER));
JScrollPane scrolling = new JScrollPane(centering, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrolling.setPreferredSize(new Dimension(100, 130));
scrolling.setPreferredSize(new Dimension(100, 200));
panel.add(scrolling, BorderLayout.CENTER);
centering.add(pairPanel);
centering.add(gridPanel);
addWorkPanel(panel);
@ -219,13 +238,15 @@ public class DebuggerConnectDialog extends DialogComponentProvider
private void itemSelected(ItemEvent evt) {
if (evt.getStateChange() == ItemEvent.DESELECTED) {
pairPanel.removeAll();
gridPanel.removeAll();
}
else if (evt.getStateChange() == ItemEvent.SELECTED) {
FactoryEntry ent = (FactoryEntry) evt.getItem();
factory = ent.factory;
currentFactory = ent.factory;
populateOptions();
//repack();
/**
* Don't repack here. It can shrink the dialog, which may not be what the user wants.
*/
}
}
@ -239,7 +260,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
setStatusText("Connecting...");
synchronized (this) {
futureConnect = factory.build();
futureConnect = currentFactory.build();
}
futureConnect.thenCompose(m -> m.fetchModelRoot()).thenAcceptAsync(r -> {
DebuggerObjectModel m = r.getModel();
@ -268,6 +289,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
synchronized (this) {
futureConnect = null;
}
successFactory = currentFactory;
connectButton.setEnabled(true);
});
}
@ -284,7 +306,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
protected synchronized CompletableFuture<DebuggerObjectModel> reset(
DebuggerModelFactory factory) {
DebuggerModelFactory factory, Program program) {
if (factory != null) {
synchronized (factories) {
dropdownModel.setSelectedItem(factories.get(factory));
@ -292,6 +314,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
dropdown.setEnabled(false);
}
else {
selectCompatibleFactory(program);
dropdown.setEnabled(true);
}
@ -311,18 +334,42 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
protected void populateOptions() {
description.setText(
HTML_BOLD_DESCRIPTION + HTMLUtilities.friendlyEncodeHTML(factory.getHtmlDetails()));
description.setText(HTML_BOLD_DESCRIPTION + currentFactory.getHtmlDetails());
propertyEditors.clear();
components.clear();
Map<String, Property<?>> optsMap = factory.getOptions();
//layout.setRows(Math.max(1, optsMap.size()));
pairPanel.removeAll();
Map<String, Property<?>> optsMap = currentFactory.getOptions();
gridPanel.removeAll();
GridBagConstraints constraints;
if (optsMap.isEmpty()) {
JLabel label =
new JLabel("<html>There are no configuration options for this connector.");
constraints = new GridBagConstraints();
gridPanel.add(label, constraints);
}
int i = 0;
for (Map.Entry<String, Property<?>> opt : optsMap.entrySet()) {
Property<?> property = opt.getValue();
JLabel label = new JLabel(opt.getKey());
pairPanel.add(label);
JLabel label = new JLabel("<html>" + HTMLUtilities.escapeHTML(opt.getKey())) {
@Override
public Dimension getPreferredSize() {
View v = (View) getClientProperty("html");
if (v == null) {
return super.getPreferredSize();
}
v.setSize(200, 0);
float height = v.getPreferredSpan(View.Y_AXIS);
return new Dimension(200, (int) height);
}
};
constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.BOTH;
constraints.gridx = 0;
constraints.gridy = i;
constraints.insets = new Insets(i == 0 ? 0 : 5, 0, 0, 5);
gridPanel.add(label, constraints);
Class<?> type = property.getValueClass();
PropertyEditor editor = PropertyEditorManager.findEditor(type);
@ -331,11 +378,22 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
editor.setValue(property.getValue());
editor.addPropertyChangeListener(this);
Component comp = MiscellaneousUtils.getEditorComponent(editor);
pairPanel.add(comp);
Component editorComponent = MiscellaneousUtils.getEditorComponent(editor);
if (editorComponent instanceof JTextField textField) {
textField.setColumns(13);
}
constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.anchor = GridBagConstraints.WEST;
constraints.gridx = 1;
constraints.gridy = i;
constraints.insets = new Insets(i == 0 ? 0 : 5, 0, 0, 0);
gridPanel.add(editorComponent, constraints);
propertyEditors.put(property, editor);
components.put(property, comp);
components.put(property, editorComponent);
i++;
}
}
@ -350,24 +408,75 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
public void writeConfigState(SaveState saveState) {
if (factory != null) {
saveState.putString(KEY_FACTORY_CLASSNAME, factory.getClass().getName());
if (currentFactory != null) {
saveState.putString(KEY_CURRENT_FACTORY_CLASSNAME, currentFactory.getClass().getName());
}
if (successFactory != null) {
saveState.putString(KEY_SUCCESS_FACTORY_CLASSNAME, successFactory.getClass().getName());
}
}
public void readConfigState(SaveState saveState) {
String factoryName = saveState.getString(KEY_FACTORY_CLASSNAME, null);
if (factoryName == null) {
return;
}
protected FactoryEntry getByName(String className) {
synchronized (factories) {
for (Map.Entry<DebuggerModelFactory, FactoryEntry> ent : factories.entrySet()) {
String name = ent.getKey().getClass().getName();
if (factoryName.equals(name)) {
factory = ent.getKey();
dropdown.setSelectedItem(ent.getValue());
for (FactoryEntry ent : factories.values()) {
String name = ent.factory.getClass().getName();
if (className.equals(name)) {
return ent;
}
}
return null;
}
}
protected Collection<PrioritizedFactory> getByPriority(Program program) {
synchronized (factories) {
return factories.values()
.stream()
.map(e -> new PrioritizedFactory(e, program))
.sorted(Comparator.comparing(pf -> -pf.priority()))
.toList();
}
}
protected PrioritizedFactory getFirstCompatibleByPriority(Program program) {
for (PrioritizedFactory pf : getByPriority(program)) {
if (pf.priority >= 0) {
return pf;
}
return null;
}
return null;
}
protected void selectCompatibleFactory(Program program) {
if (currentFactory != null && currentFactory.isCompatible(program)) {
return;
}
if (successFactory != null && successFactory.isCompatible(program)) {
currentFactory = successFactory;
synchronized (factories) {
dropdown.setSelectedItem(factories.get(successFactory));
}
return;
}
PrioritizedFactory compat = getFirstCompatibleByPriority(program);
if (compat == null) {
return;
}
currentFactory = compat.entry.factory;
dropdown.setSelectedItem(compat.entry);
}
public void readConfigState(SaveState saveState) {
String currentFactoryName = saveState.getString(KEY_CURRENT_FACTORY_CLASSNAME, null);
FactoryEntry restoreCurrent =
currentFactoryName == null ? null : getByName(currentFactoryName);
currentFactory = restoreCurrent == null ? null : restoreCurrent.factory;
dropdown.setSelectedItem(restoreCurrent);
String successFactoryName = saveState.getString(KEY_SUCCESS_FACTORY_CLASSNAME, null);
FactoryEntry restoreSuccess =
successFactoryName == null ? null : getByName(successFactoryName);
successFactory = restoreSuccess == null ? null : restoreSuccess.factory;
}
}

View file

@ -641,14 +641,24 @@ public class DebuggerModelServicePlugin extends Plugin
}
protected CompletableFuture<DebuggerObjectModel> doShowConnectDialog(PluginTool tool,
DebuggerModelFactory factory) {
CompletableFuture<DebuggerObjectModel> future = connectDialog.reset(factory);
DebuggerModelFactory factory, Program program) {
CompletableFuture<DebuggerObjectModel> future = connectDialog.reset(factory, program);
tool.showDialog(connectDialog);
return future;
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog() {
return doShowConnectDialog(tool, null, null);
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog(Program program) {
return doShowConnectDialog(tool, null, program);
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog(DebuggerModelFactory factory) {
return doShowConnectDialog(tool, factory);
return doShowConnectDialog(tool, factory, null);
}
}

View file

@ -269,9 +269,19 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
closeAllModels();
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog() {
return delegate.doShowConnectDialog(tool, null, null);
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog(Program program) {
return delegate.doShowConnectDialog(tool, null, program);
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog(DebuggerModelFactory factory) {
return delegate.doShowConnectDialog(tool, factory);
return delegate.doShowConnectDialog(tool, factory, null);
}
@Override

View file

@ -353,14 +353,20 @@ public interface DebuggerModelService {
*
* @return a future which completes with the new connection, possibly cancelled
*/
default CompletableFuture<DebuggerObjectModel> showConnectDialog() {
return showConnectDialog(null);
}
CompletableFuture<DebuggerObjectModel> showConnectDialog();
/**
* Prompt the user to create a new connection, hinting at the program to launch
*
* @param program the current program used to help select a default
* @return a future which completes with the new connection, possibly cancelled
*/
CompletableFuture<DebuggerObjectModel> showConnectDialog(Program program);
/**
* Prompt the user to create a new connection, optionally fixing the factory
*
* @param factory the required factory, or null for user selection
* @param factory the required factory
* @return a future which completes with the new connection, possible cancelled
*/
CompletableFuture<DebuggerObjectModel> showConnectDialog(DebuggerModelFactory factory);

View file

@ -473,15 +473,15 @@ public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITes
DebuggerConnectDialog dialog = waitForDialogComponent(DebuggerConnectDialog.class);
FactoryEntry fe = (FactoryEntry) dialog.dropdownModel.getSelectedItem();
assertEquals(mb.testFactory, fe.factory);
assertEquals(mb.testFactory, fe.factory());
assertEquals(TestDebuggerModelFactory.FAKE_DETAILS_HTML, dialog.description.getText());
Component[] components = dialog.pairPanel.getComponents();
Component[] components = dialog.gridPanel.getComponents();
assertTrue(components[0] instanceof JLabel);
JLabel label = (JLabel) components[0];
assertEquals(TestDebuggerModelFactory.FAKE_OPTION_NAME, label.getText());
assertEquals("<html>" + TestDebuggerModelFactory.FAKE_OPTION_NAME, label.getText());
assertTrue(components[1] instanceof JTextField);
JTextField field = (JTextField) components[1];
@ -518,7 +518,7 @@ public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITes
DebuggerConnectDialog connectDialog = waitForDialogComponent(DebuggerConnectDialog.class);
FactoryEntry fe = (FactoryEntry) connectDialog.dropdownModel.getSelectedItem();
assertEquals(mb.testFactory, fe.factory);
assertEquals(mb.testFactory, fe.factory());
pressButtonByText(connectDialog, AbstractConnectAction.NAME, true);
// NOTE: testModel is null. Don't use #createTestModel(), which adds to service

View file

@ -16,22 +16,52 @@
package ghidra.dbg;
import ghidra.dbg.util.ConfigurableFactory;
import ghidra.program.model.listing.Program;
import ghidra.util.classfinder.ExtensionPoint;
/**
* A factory for a debugger model
*
* <p>
* This provides a discoverable means of creating a debug model.
* This provides a discoverable means of configuring and creating a debug model.
*/
public interface DebuggerModelFactory
extends ExtensionPoint, ConfigurableFactory<DebuggerObjectModel> {
/**
* Check if this factory is compatible with the local system.
* Get the priority for selecting this factory by default for the given program
*
* <p>
* A default factory is selected when the current factory and the last successful factory are
* incompatible with the current program, or if this is the very first time connecting. Of those
* factories compatible with the current program, the one with the highest priority (larger
* numerical value) is selected. If none are compatible, then the current selection is left as
* is.
*
* <p>
* Note that negative priorities imply the factory is not compatible with the given program or
* local system.
*
* @param program the current program, or null
* @return the priority, higher values mean higher priority
*/
default int getPriority(Program program) {
return 0;
}
/**
* Check if this factory is compatible with the local system and given program.
*
* <p>
* <b>WARNING:</b> Implementations should not likely override this method. If one does, it must
* behave in the same manner as given in this default implementation: If
* {@link #getPriority(Program)} would return a non-negative result for the program, then this
* factory is compatible with that program. If negative, this factory is not compatible.
*
* @param program the current program, or null
* @return true if compatible
*/
default boolean isCompatible() {
return true;
default boolean isCompatible(Program program) {
return getPriority(program) >= 0;
}
}

View file

@ -27,7 +27,7 @@ import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
public class TestDebuggerModelFactory implements DebuggerModelFactory {
public static final String FAKE_DETAILS = "A 'connection' to a fake debugger";
public static final String FAKE_DETAILS_HTML =
"<html><b>Description:</b> A&nbsp;'connection'&nbsp;to&nbsp;a&nbsp;fake&nbsp;debugger";
"<html><b>Description:</b> A 'connection' to a fake debugger";
public static final String FAKE_OPTION_NAME = "Test String";
public static final String FAKE_DEFAULT = "Default test string";