diff --git a/Ghidra/Debug/Debugger-dap/Module.manifest b/Ghidra/Debug/Debugger-dap/Module.manifest
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Ghidra/Debug/Debugger-dap/README.md b/Ghidra/Debug/Debugger-dap/README.md
new file mode 100644
index 0000000000..a4c1d98782
--- /dev/null
+++ b/Ghidra/Debug/Debugger-dap/README.md
@@ -0,0 +1 @@
+# Debugger-DAP: provides server-side DAP access to the debugger agents
diff --git a/Ghidra/Debug/Debugger-dap/build.gradle b/Ghidra/Debug/Debugger-dap/build.gradle
new file mode 100644
index 0000000000..769996701d
--- /dev/null
+++ b/Ghidra/Debug/Debugger-dap/build.gradle
@@ -0,0 +1,30 @@
+/* ###
+ * 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.
+ */
+
+apply from: "${rootProject.projectDir}/gradle/javaProject.gradle"
+apply from: "${rootProject.projectDir}/gradle/helpProject.gradle"
+apply from: "${rootProject.projectDir}/gradle/distributableGhidraModule.gradle"
+
+apply plugin: 'eclipse'
+eclipse.project.name = 'Debug Debugger-dap'
+
+dependencies {
+ api project(':Debugger-rmi-trace')
+ api "org.eclipse.lsp4j:org.eclipse.lsp4j:1.0.0"
+ api "org.eclipse.lsp4j:org.eclipse.lsp4j.debug:1.0.0"
+ api "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:1.0.0"
+ api "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc.debug:1.0.0"
+}
diff --git a/Ghidra/Debug/Debugger-dap/certification.manifest b/Ghidra/Debug/Debugger-dap/certification.manifest
new file mode 100644
index 0000000000..70353c8de8
--- /dev/null
+++ b/Ghidra/Debug/Debugger-dap/certification.manifest
@@ -0,0 +1,5 @@
+##VERSION: 2.0
+Module.manifest||GHIDRA||||END|
+README.md||GHIDRA||||END|
+src/main/help/help/TOC_Source.xml||GHIDRA||||END|
+src/main/help/help/topics/dap/dap.html||GHIDRA||||END|
diff --git a/Ghidra/Debug/Debugger-dap/src/main/help/help/TOC_Source.xml b/Ghidra/Debug/Debugger-dap/src/main/help/help/TOC_Source.xml
new file mode 100644
index 0000000000..6fe280bf6d
--- /dev/null
+++ b/Ghidra/Debug/Debugger-dap/src/main/help/help/TOC_Source.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Debug/Debugger-dap/src/main/help/help/topics/dap/dap.html b/Ghidra/Debug/Debugger-dap/src/main/help/help/topics/dap/dap.html
new file mode 100644
index 0000000000..6c4bfe27c5
--- /dev/null
+++ b/Ghidra/Debug/Debugger-dap/src/main/help/help/topics/dap/dap.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+ Debugger Adapter Protocol (DAP)
+
+
+
+
+
+ DAP Server
+
+ The DebuggerDapServerPlugin provides a very simple adapter allowing access to the Ghidra
+ debugger by standard DAP clients. The plugin provides two actions, Start server and
+ Stop server , and two options, IP Address and Port . The server will remain
+ running until the stop command is issued. Ghidra debugger agents may launch and kill sessions
+ without restarting the server, and DAP clients may connect, disconnect, and reconnect. Running
+ multiple sessions simultaneously has not been tested extensively and requires, at a minimum,
+ changing port values.
+
+ DAP Clients
+
+ Exact configuration and use of the DAP client will be largely client-dependent, particularly
+ with regard to supported commands. Attach and launch are again largely untested,
+ as the server assumes an active agent session. As a starting example, the DAP server may be
+ accessed from VSCode by writing a config.yaml file, and pointing a new
+ or existing debuggger configuration at it. For example, pointing "Python Debugger: Remote
+ Attach" to the following config.yaml will allow F5 to establish a local
+ connection to a DAP server running at port 54321 :
+
+
+
+
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Python Debugger: Remote Attach",
+ "type": "debugpy",
+ "request": "attach",
+ "connect": {
+ "host": "localhost",
+ "port": 54321
+ },
+ "pathMappings": [
+ {
+ "localRoot": "${workspaceFolder}",
+ "remoteRoot": "."
+ }
+ ]
+ }
+ ]
+}
+
+
+
+
+
diff --git a/Ghidra/Debug/Debugger-dap/src/main/java/dap/DapDebugAdapter.java b/Ghidra/Debug/Debugger-dap/src/main/java/dap/DapDebugAdapter.java
new file mode 100644
index 0000000000..19c445d409
--- /dev/null
+++ b/Ghidra/Debug/Debugger-dap/src/main/java/dap/DapDebugAdapter.java
@@ -0,0 +1,885 @@
+/* ###
+ * 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 dap;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.lsp4j.debug.*;
+import org.eclipse.lsp4j.debug.StackFrame;
+import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
+import org.eclipse.lsp4j.debug.services.IDebugProtocolServer;
+
+import docking.ActionContext;
+import ghidra.app.plugin.core.debug.gui.action.BasicAutoReadMemorySpec;
+import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget;
+import ghidra.app.services.*;
+import ghidra.debug.api.model.DebuggerSingleObjectPathActionContext;
+import ghidra.debug.api.target.ActionName;
+import ghidra.debug.api.target.Target.ActionEntry;
+import ghidra.debug.api.target.Target.ObjectArgumentPolicy;
+import ghidra.debug.api.tracemgr.DebuggerCoordinates;
+import ghidra.debug.api.tracermi.*;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.program.database.sourcemap.SourceFile;
+import ghidra.program.model.address.*;
+import ghidra.program.model.lang.Register;
+import ghidra.program.model.lang.RegisterValue;
+import ghidra.program.model.listing.*;
+import ghidra.program.model.mem.MemoryAccessException;
+import ghidra.program.model.sourcemap.SourceFileManager;
+import ghidra.program.model.sourcemap.SourceMapEntry;
+import ghidra.program.model.symbol.Symbol;
+import ghidra.program.model.symbol.SymbolIterator;
+import ghidra.program.util.ProgramLocation;
+import ghidra.trace.model.Lifespan;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.breakpoint.TraceBreakpointKind.CommonSet;
+import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
+import ghidra.trace.model.breakpoint.TraceBreakpointLocation;
+import ghidra.trace.model.guest.TracePlatform;
+import ghidra.trace.model.modules.TraceModule;
+import ghidra.trace.model.modules.TraceModuleManager;
+import ghidra.trace.model.stack.TraceStack;
+import ghidra.trace.model.stack.TraceStackFrame;
+import ghidra.trace.model.target.TraceObject;
+import ghidra.trace.model.target.iface.TraceObjectInterface;
+import ghidra.trace.model.thread.*;
+import ghidra.util.Msg;
+import ghidra.util.NumericUtilities;
+
+public class DapDebugAdapter implements IDebugProtocolServer {
+
+ private DapPlugin plugin;
+ private DebuggerTraceManagerService manager;
+ private TraceRmiService rmi;
+ private DebuggerStaticMappingService mappings;
+
+ private Trace trace;
+ private TraceRmiTarget target;
+ private TraceRmiConnection conn;
+ private RemoteMethodRegistry registry;
+
+ private Map dataBreakpoints = new HashMap<>();
+ private AddressSpace space;
+ private IDebugProtocolClient client;
+
+ public DapDebugAdapter(DapPlugin plugin, PluginTool tool) {
+ this.plugin = plugin;
+ this.manager = tool.getService(DebuggerTraceManagerService.class);
+ this.rmi = tool.getService(TraceRmiService.class);
+ this.mappings = tool.getService(DebuggerStaticMappingService.class);
+ setCoordinates(manager.getCurrentFor(manager.getCurrentTrace()));
+ }
+
+ public void setCoordinates(DebuggerCoordinates coordinates) {
+ Trace traceFromCoord = coordinates.getTrace();
+ if (traceFromCoord == null || traceFromCoord.equals(this.trace)) {
+ return;
+ }
+ this.trace = traceFromCoord;
+ this.target = (TraceRmiTarget) coordinates.getTarget();
+
+ space = trace.getBaseAddressFactory().getDefaultAddressSpace();
+ for (TraceRmiConnection c : rmi.getAllConnections()) {
+ if (c.getTargets().contains(target)) {
+ this.conn = c;
+ break;
+ }
+ }
+ this.registry = conn.getMethods();
+ plugin.addListener(trace);
+ }
+
+ @Override
+ public CompletableFuture initialize(InitializeRequestArguments args) {
+ Capabilities capabilities = new Capabilities();
+ capabilities.setSupportsConfigurationDoneRequest(true);
+ //capabilities.setSupportsConditionalBreakpoints(true);
+ capabilities.setSupportsDataBreakpoints(true);
+ capabilities.setSupportsDataBreakpointBytes(true);
+ capabilities.setSupportsFunctionBreakpoints(true);
+ capabilities.setSupportsInstructionBreakpoints(true);
+ capabilities.setSupportsModulesRequest(true);
+ capabilities.setSupportsReadMemoryRequest(true);
+ capabilities.setSupportsWriteMemoryRequest(true);
+ capabilities.setSupportsTerminateRequest(true);
+ capabilities.setSupportsDisassembleRequest(true);
+ client.initialized();
+ return CompletableFuture.completedFuture(capabilities);
+ }
+
+ @Override
+ public CompletableFuture disconnect(DisconnectArguments args) {
+ plugin.removeListener(trace);
+ this.trace = null;
+ return CompletableFuture.completedFuture(null);
+ }
+
+ @Override
+ public CompletableFuture continue_(ContinueArguments args) {
+ TraceThread thread = getThread(args.getThreadId());
+ if (thread == null || thread.getObject() == null) {
+ return CompletableFuture.failedFuture(
+ new IllegalStateException("No active thread context found."));
+ }
+
+ ActionContext context =
+ new DebuggerSingleObjectPathActionContext(thread.getObject().getCanonicalPath());
+ return invoke(ActionName.RESUME, context).thenApply(_ -> {
+ ContinueResponse response = new ContinueResponse();
+ response.setAllThreadsContinued(true);
+ return response;
+ });
+ }
+
+ @Override
+ public CompletableFuture reverseContinue(ReverseContinueArguments args) {
+ // TODO: We should standardize this
+ RemoteMethod method0 = registry.get("go_back");
+ RemoteMethod method1 = registry.get("resume_back");
+ if (method0 == null && method1 == null) {
+ return CompletableFuture.failedFuture(
+ new UnsupportedOperationException(
+ "The connected backend does not support the 'reverse continue' method."));
+ }
+
+ RemoteMethod method = method0 == null ? method1 : method0;
+ return method.invokeAsync(new HashMap<>()).toCompletableFuture().thenApply(_ -> null);
+ }
+
+ @Override
+ public CompletableFuture next(NextArguments args) {
+ TraceThread thread = getThread(args.getThreadId());
+ if (thread == null || thread.getObject() == null) {
+ return CompletableFuture.failedFuture(
+ new IllegalStateException("No active thread context found."));
+ }
+
+ ActionContext context =
+ new DebuggerSingleObjectPathActionContext(thread.getObject().getCanonicalPath());
+ return invoke(ActionName.STEP_OVER, context);
+ }
+
+ @Override
+ public CompletableFuture stepIn(StepInArguments args) {
+ TraceThread thread = getThread(args.getThreadId());
+ if (thread == null || thread.getObject() == null) {
+ return CompletableFuture.failedFuture(
+ new IllegalStateException("No active thread context found."));
+ }
+
+ ActionContext context =
+ new DebuggerSingleObjectPathActionContext(thread.getObject().getCanonicalPath());
+ return invoke(ActionName.STEP_INTO, context);
+ }
+
+ @Override
+ public CompletableFuture stepOut(StepOutArguments args) {
+ TraceThread thread = getThread(args.getThreadId());
+ if (thread == null || thread.getObject() == null) {
+ return CompletableFuture.failedFuture(
+ new IllegalStateException("No active thread context found."));
+ }
+
+ ActionContext context =
+ new DebuggerSingleObjectPathActionContext(thread.getObject().getCanonicalPath());
+ return invoke(ActionName.STEP_OUT, context);
+ }
+
+ @Override
+ public CompletableFuture stepBack(StepBackArguments args) {
+ TraceThread thread = getThread(args.getThreadId());
+ if (thread == null || thread.getObject() == null) {
+ return CompletableFuture.failedFuture(
+ new IllegalStateException("No active thread context found."));
+ }
+
+ ActionContext context =
+ new DebuggerSingleObjectPathActionContext(thread.getObject().getCanonicalPath());
+ return invoke(ActionName.STEP_BACK, context);
+ }
+
+ @Override
+ public CompletableFuture pause(PauseArguments args) {
+ TraceProcess proc = getProcess();
+ ActionContext context =
+ new DebuggerSingleObjectPathActionContext(proc.getObject().getCanonicalPath());
+ return invoke(ActionName.INTERRUPT, context);
+ }
+
+ @Override
+ public CompletableFuture setInstructionBreakpoints(
+ SetInstructionBreakpointsArguments args) {
+ return clearExistingBreakpointsOfType(false)
+ .thenCompose(_ -> {
+ List> futuresList = new ArrayList<>();
+ InstructionBreakpoint[] breakpoints = args.getBreakpoints();
+ for (InstructionBreakpoint bpt : breakpoints) {
+ String ref = bpt.getInstructionReference();
+ TraceBreakpointKindSet kind = kind(bpt.getMode());
+ String condition = bpt.getCondition();
+ try {
+ Address address = space.getAddress(ref).add(bpt.getOffset());
+ CompletableFuture future = target.placeBreakpointAsync(
+ new AddressRangeImpl(address, 1), kind, condition, null);
+ futuresList.add(future);
+ }
+ catch (Exception e) {
+ // Drop malformed entries (excluded entries in the response
+ // should be apparent to the client)
+ }
+ }
+ return CompletableFuture.allOf(futuresList.toArray(new CompletableFuture[0]));
+ })
+ .thenApply(_ -> {
+ List rbpts = getBreakpointList();
+ SetInstructionBreakpointsResponse response =
+ new SetInstructionBreakpointsResponse();
+ response.setBreakpoints(rbpts.toArray(new Breakpoint[0]));
+ return response;
+ });
+ }
+
+ public record DataBreakpointInfo(String key, String mode, int size) {}
+
+ @Override
+ public CompletableFuture dataBreakpointInfo(
+ DataBreakpointInfoArguments args) {
+ Boolean asAddress = args.getAsAddress();
+ Integer size = args.getBytes();
+ String mode = args.getMode();
+ String name = args.getName();
+
+ if (asAddress == null || !asAddress) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ dataBreakpoints.put(name, new DataBreakpointInfo(name, mode, size != null ? size : 1));
+
+ DataBreakpointInfoResponse response = new DataBreakpointInfoResponse();
+ response.setDescription(name);
+ response.setDataId(name);
+ response.setAccessTypes(DataBreakpointAccessType.values());
+ return CompletableFuture.completedFuture(response);
+ }
+
+ @Override
+ public CompletableFuture setBreakpoints(SetBreakpointsArguments args) {
+ Program program = plugin.getCurrentProgram();
+ if (program == null) {
+ return CompletableFuture.failedFuture(
+ new IllegalStateException("No active program source file trace map."));
+ }
+ return clearExistingBreakpointsOfType(false)
+ .thenCompose(_ -> {
+ List> futuresList = new ArrayList<>();
+ convertAndPlaceBreakpoints(args, program, futuresList);
+ return CompletableFuture.allOf(futuresList.toArray(new CompletableFuture[0]));
+ })
+ .thenApply(_ -> {
+ List rbpts = getBreakpointList();
+ SetBreakpointsResponse response = new SetBreakpointsResponse();
+ response.setBreakpoints(rbpts.toArray(new Breakpoint[0]));
+ return response;
+ });
+ }
+
+ private void convertAndPlaceBreakpoints(SetBreakpointsArguments args, Program program,
+ List> futuresList) {
+ SourceFileManager sourceManager = program.getSourceFileManager();
+ SourceBreakpoint[] breakpoints = args.getBreakpoints();
+ for (SourceBreakpoint bpt : breakpoints) {
+ int ref = bpt.getLine();
+ List files = sourceManager.getMappedSourceFiles();
+ for (SourceFile f : files) {
+ List entries =
+ sourceManager.getSourceMapEntries(f, ref);
+ for (SourceMapEntry entry : entries) {
+ convertAndPlaceBreakpoint(program, futuresList, bpt, entry);
+ }
+ }
+ }
+ }
+
+ private void convertAndPlaceBreakpoint(Program program,
+ List> futuresList,
+ SourceBreakpoint bpt, SourceMapEntry entry) {
+ try {
+ Address baseAddress = entry.getBaseAddress();
+ Address targetAddress = dynamicForStatic(program, baseAddress);
+ if (targetAddress != null) {
+ CompletableFuture future = target.placeBreakpointAsync(
+ new AddressRangeImpl(targetAddress, 1),
+ kind(bpt.getMode()),
+ bpt.getCondition(), null);
+ futuresList.add(future);
+ }
+ }
+ catch (Exception e) {
+ // Drop malformed entries
+ }
+ }
+
+ @Override
+ public CompletableFuture setDataBreakpoints(
+ SetDataBreakpointsArguments args) {
+ return clearExistingBreakpointsOfType(true)
+ .thenCompose(_ -> {
+ List> futuresList = new ArrayList<>();
+ DataBreakpoint[] breakpoints = args.getBreakpoints();
+ for (DataBreakpoint bpt : breakpoints) {
+ String ref = bpt.getDataId();
+ DataBreakpointInfo info = dataBreakpoints.get(ref);
+ DataBreakpointAccessType accessType = bpt.getAccessType();
+ String condition = bpt.getCondition();
+
+ try {
+ Address address = space.getAddress(ref);
+ CompletableFuture future = target.placeBreakpointAsync(
+ new AddressRangeImpl(address, info.size()), kinds(accessType),
+ condition, null);
+ futuresList.add(future);
+ }
+ catch (Exception e) {
+ // Drop malformed entries
+ }
+ }
+ return CompletableFuture.allOf(futuresList.toArray(new CompletableFuture[0]));
+ })
+ .thenApply(_ -> {
+ List rbpts = getBreakpointList();
+ SetDataBreakpointsResponse response = new SetDataBreakpointsResponse();
+ response.setBreakpoints(rbpts.toArray(new Breakpoint[0]));
+ return response;
+ });
+ }
+
+ @Override
+ public CompletableFuture setFunctionBreakpoints(
+ SetFunctionBreakpointsArguments args) {
+ return clearExistingBreakpointsOfType(false)
+ .thenCompose(_ -> {
+ List> futuresList = new ArrayList<>();
+ FunctionBreakpoint[] breakpoints = args.getBreakpoints();
+ for (FunctionBreakpoint bpt : breakpoints) {
+ Function f = getFunctionByName(bpt.getName());
+ if (f == null) {
+ continue;
+ }
+ Address targetAddress = dynamicForStatic(f.getProgram(), f.getEntryPoint());
+ if (targetAddress == null) {
+ continue;
+ }
+ try {
+ CompletableFuture future = target.placeBreakpointAsync(
+ new AddressRangeImpl(targetAddress, 1),
+ CommonSet.SWX.kinds(),
+ bpt.getCondition(), null);
+ futuresList.add(future);
+ break;
+ }
+ catch (Exception e) {
+ // Drop malformed entries
+ }
+ }
+ return CompletableFuture.allOf(futuresList.toArray(new CompletableFuture[0]));
+ })
+ .thenApply(_ -> {
+ List rbpts = getBreakpointList();
+ SetFunctionBreakpointsResponse response =
+ new SetFunctionBreakpointsResponse();
+ response.setBreakpoints(rbpts.toArray(new Breakpoint[0]));
+ return response;
+ });
+ }
+
+ private CompletableFuture clearExistingBreakpointsOfType(boolean dataBreakpoints) {
+ // TODO: Do we really want to do this?
+ // Right now, it's not obvious that the DAP client knows about all breakpoints, i.e.
+ // unclear whether it receives/filters client.breakpoint messages. Until it does, seems
+ // unwise to clear breakpoints possible only know to Ghidra
+ List> deletionFutures = new ArrayList<>();
+ return CompletableFuture.allOf(deletionFutures.toArray(new CompletableFuture[0]));
+ }
+
+ private Function getFunctionByName(String name) {
+ Program program = plugin.getCurrentProgram();
+ if (program == null || name == null) {
+ return null;
+ }
+ SymbolIterator symbols = program.getSymbolTable().getSymbols(name);
+ while (symbols.hasNext()) {
+ Symbol sym = symbols.next();
+ if (sym.getObject() instanceof Function func) {
+ return func;
+ }
+ }
+ return null;
+ }
+
+ private Address dynamicForStatic(Program program, Address staticAddress) {
+ ProgramLocation dynamicLocation = mappings.getDynamicLocationFromStatic(
+ trace.getProgramView(), new ProgramLocation(program, staticAddress));
+ return dynamicLocation == null ? null : dynamicLocation.getAddress();
+ }
+
+ @Override
+ public CompletableFuture stackTrace(StackTraceArguments args) {
+ Integer startFrame = args.getStartFrame();
+ Integer levels = args.getLevels();
+ int tid = args.getThreadId();
+ // TOOO: StackFrameFormat format = args.getFormat();
+
+ TraceThread thread = getThread(tid);
+ if (thread == null || thread.getObject() == null) {
+ return CompletableFuture.failedFuture(
+ new IllegalStateException("No active thread context found."));
+ }
+
+ long currentSnap = manager.getCurrentSnap();
+ TraceStack stack = trace.getStackManager().getLatestStack(thread, currentSnap);
+ if (stack == null) {
+ return CompletableFuture.completedFuture(new StackTraceResponse());
+ }
+
+ StackTraceResponse response = new StackTraceResponse();
+ List rframes = new ArrayList<>();
+ int start = (startFrame != null) ? startFrame : 0;
+ int maxLevels = (levels != null && levels > 0) ? levels : stack.getDepth(currentSnap);
+
+ for (TraceStackFrame f : stack.getFrames(currentSnap)) {
+ if (f.getLevel() >= start && rframes.size() < maxLevels) {
+ StackFrame rf = new StackFrame();
+ rf.setId(f.getLevel());
+
+ String desc = getDisplay(f.getObject());
+ if (desc != null) {
+ rf.setName(desc);
+ }
+
+ rf.setInstructionPointerReference(
+ f.getProgramCounter(currentSnap).toString());
+ rframes.add(rf);
+ }
+ }
+ response.setStackFrames(rframes.toArray(new StackFrame[0]));
+ return CompletableFuture.completedFuture(response);
+ }
+
+ @Override
+ public CompletableFuture threads() {
+ if (trace == null) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ TraceThreadManager threadManager = trace.getThreadManager();
+ Collection extends TraceThread> threads =
+ threadManager.getLiveThreads(manager.getCurrentSnap());
+
+ ThreadsResponse response = new ThreadsResponse();
+ List rthreads = new ArrayList<>();
+
+ for (TraceThread t : threads) {
+ org.eclipse.lsp4j.debug.Thread rt = new org.eclipse.lsp4j.debug.Thread();
+ rt.setId(getThreadId(t));
+ rt.setName(t.getName(manager.getCurrentSnap()));
+ rthreads.add(rt);
+ }
+ response.setThreads(rthreads.toArray(new org.eclipse.lsp4j.debug.Thread[0]));
+ return CompletableFuture.completedFuture(response);
+ }
+
+ @Override
+ public CompletableFuture modules(ModulesArguments args) {
+ if (trace == null) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ TraceModuleManager modManager = trace.getModuleManager();
+ Collection extends TraceModule> modules =
+ modManager.getLoadedModules(manager.getCurrentSnap());
+
+ ModulesResponse response = new ModulesResponse();
+ List rmods = new ArrayList<>();
+
+ for (TraceModule m : modules) {
+ org.eclipse.lsp4j.debug.Module rm = new org.eclipse.lsp4j.debug.Module();
+ rm.setId((int) m.getObject().getKey());
+ rm.setName(m.getName(manager.getCurrentSnap()));
+ rm.setAddressRange(m.getRange(manager.getCurrentSnap()).toString());
+ rmods.add(rm);
+ }
+ response.setModules(rmods.toArray(new org.eclipse.lsp4j.debug.Module[0]));
+ return CompletableFuture.completedFuture(response);
+ }
+
+ @Override
+ public CompletableFuture evaluate(EvaluateArguments args) {
+ return target.executeAsync(args.getExpression(), true)
+ .toCompletableFuture()
+ .thenApply(result -> {
+ EvaluateResponse response = new EvaluateResponse();
+ response.setResult(result != null ? result.toString() : "null");
+ return response;
+ });
+ }
+
+ @Override
+ public CompletableFuture terminate(TerminateArguments args) {
+ return target.disconnectAsync();
+ }
+
+ @Override
+ public CompletableFuture readMemory(ReadMemoryArguments args) {
+ Address baseAddress = getAddress(args.getMemoryReference());
+ int requestedCount = args.getCount();
+ long offsetAdjustment = (args.getOffset() != null) ? args.getOffset() : 0L;
+
+ final Address address;
+ try {
+ address = baseAddress.add(offsetAdjustment);
+ }
+ catch (AddressOutOfBoundsException e) {
+ return CompletableFuture.failedFuture(e);
+ }
+
+ AddressSetView view;
+ try {
+ view = new AddressSet(address, address.add(requestedCount - 1));
+ }
+ catch (AddressOutOfBoundsException e) {
+ return CompletableFuture.failedFuture(e);
+ }
+
+ return BasicAutoReadMemorySpec.VISIBLE
+ .readMemory(plugin.getTool(), manager.getCurrentFor(trace), view)
+ .thenApply(_ -> {
+ ByteBuffer buf = ByteBuffer.allocate(args.getCount());
+ int bytesRead =
+ trace.getMemoryManager()
+ .getViewBytes(manager.getCurrentSnap(), address, buf);
+ buf.flip();
+
+ ReadMemoryResponse response = new ReadMemoryResponse();
+ response.setAddress(address.toString());
+ String data =
+ new String(Base64.getEncoder().encode(buf).limit(bytesRead).array());
+ response.setData(data);
+ response.setUnreadableBytes(args.getCount() - bytesRead);
+ return response;
+ });
+ }
+
+ @Override
+ public CompletableFuture writeMemory(WriteMemoryArguments args) {
+ Address baseAddress = getAddress(args.getMemoryReference());
+ long offsetAdjustment = (args.getOffset() != null) ? args.getOffset() : 0L;
+
+ final Address address;
+ try {
+ address = baseAddress.add(offsetAdjustment);
+ }
+ catch (AddressOutOfBoundsException e) {
+ return CompletableFuture.failedFuture(e);
+ }
+
+ byte[] bytes = Base64.getDecoder().decode(args.getData());
+ return target.writeMemoryAsync(address, bytes).thenApply(_ -> {
+ WriteMemoryResponse response = new WriteMemoryResponse();
+ response.setOffset(args.getOffset());
+ response.setBytesWritten(bytes.length);
+ return response;
+ });
+ }
+
+ @Override
+ public CompletableFuture loadedSources(LoadedSourcesArguments args) {
+ SourceFileManager sourceManager = plugin.getCurrentProgram().getSourceFileManager();
+ List mappedSourceFiles = sourceManager.getMappedSourceFiles();
+ LoadedSourcesResponse response = new LoadedSourcesResponse();
+ List sources = new ArrayList<>();
+
+ for (SourceFile f : mappedSourceFiles) {
+ Source src = new Source();
+ src.setName(f.getFilename());
+ src.setPath(f.getPath());
+ sources.add(src);
+ }
+ response.setSources(sources.toArray(new Source[0]));
+ return CompletableFuture.completedFuture(response);
+ }
+
+ @Override
+ public CompletableFuture disassemble(DisassembleArguments args) {
+ Listing listing = trace.getProgramView().getListing();
+ String memoryReference = args.getMemoryReference();
+ Address start = getAddress(memoryReference);
+ int count = args.getInstructionCount();
+ InstructionIterator iter = listing.getInstructions(start, true);
+ List instructions = new ArrayList<>();
+ while (iter.hasNext() && instructions.size() < count) {
+ instructions.add(iter.next());
+ }
+
+ DisassembleResponse response = new DisassembleResponse();
+ DisassembledInstruction[] rinstructions = new DisassembledInstruction[instructions.size()];
+ int n = 0;
+ for (Instruction i : instructions) {
+ DisassembledInstruction ri = new DisassembledInstruction();
+ ri.setAddress(i.getAddress().toString());
+ ri.setInstruction(i.toString());
+ try {
+ String rep = NumericUtilities.convertBytesToString(i.getBytes(), ":");
+ ri.setInstructionBytes(rep);
+ }
+ catch (MemoryAccessException e) {
+ Msg.error(this, e.getMessage());
+ }
+ rinstructions[n++] = ri;
+ }
+ response.setInstructions(rinstructions);
+ return CompletableFuture.completedFuture(response);
+ }
+
+ @Override
+ public CompletableFuture goto_(GotoArguments args) {
+ TracePlatform platform = trace.getPlatformManager().getHostPlatform();
+ TraceThread thread = getThread(args.getThreadId());
+ if (thread == null) {
+ return CompletableFuture.failedFuture(
+ new IllegalStateException("Thread missing during context update."));
+ }
+ Register pc = trace.getBaseLanguage().getProgramCounter();
+ RegisterValue rval = new RegisterValue(pc, BigInteger.valueOf(args.getTargetId()));
+ return target.writeRegisterAsync(platform, thread, 0, rval);
+ }
+
+ // NOT CURRENTLY IMPLEMENTED
+
+ @Override
+ public CompletableFuture launch(Map args) {
+ RemoteMethod method = registry.get("launch");
+ if (method == null) {
+ return CompletableFuture.failedFuture(
+ new UnsupportedOperationException(
+ "The connected backend does not support the 'launch' method."));
+ }
+
+ return method.invokeAsync(args).toCompletableFuture().thenApply(_ -> null);
+ }
+
+ @Override
+ public CompletableFuture attach(Map args) {
+ RemoteMethod method = registry.get("attach");
+ if (method == null) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ return method.invokeAsync(args).toCompletableFuture().thenApply(_ -> null);
+ }
+
+ @Override
+ public CompletableFuture restart(RestartArguments args) {
+ RemoteMethod method = registry.get("launch");
+ if (method == null) {
+ return CompletableFuture.failedFuture(
+ new UnsupportedOperationException(
+ "The connected backend does not support the 'restart' method."));
+ }
+
+ return method.invokeAsync(new HashMap<>()).toCompletableFuture().thenApply(_ -> null);
+ }
+
+ @Override
+ public CompletableFuture exceptionInfo(ExceptionInfoArguments args) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ @Override
+ public CompletableFuture setExceptionBreakpoints(
+ SetExceptionBreakpointsArguments args) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ // private methods
+
+ private CompletableFuture invoke(ActionName action, ActionContext context) {
+ ActionEntry entry = target
+ .collectActions(action, context,
+ ObjectArgumentPolicy.CURRENT_AND_RELATED)
+ .values()
+ .stream()
+ .filter(e -> !e.requiresPrompt())
+ .sorted(Comparator.comparing(e -> -e.specificity()))
+ .findFirst()
+ .orElse(null);
+ if (entry != null) {
+ return entry.invokeAsyncWithoutTimeout(false).thenApply(_ -> null);
+ }
+ return CompletableFuture.failedFuture(new RuntimeException("No such method"));
+ }
+
+ public int getThreadId(TraceThread thread) {
+ var tidAttr = thread.getObject().getValue(manager.getCurrentSnap(), TraceThread.KEY_TID);
+ if (tidAttr == null || tidAttr.getValue() == null) {
+ return -1;
+ }
+
+ Long tid = (Long) tidAttr.getValue();
+ return tid.intValue();
+ }
+
+ TraceProcess getProcess() {
+ TraceThread currentThread = manager.getCurrentThread();
+ if (currentThread == null) {
+ return null;
+ }
+ return currentThread.getObject()
+ .queryAncestorsInterface(Lifespan.ALL, TraceProcess.class)
+ .findFirst()
+ .orElse(null);
+ }
+
+ private TraceThread getThread(int threadId) {
+ for (TraceThread t : trace.getThreadManager().getAllThreads()) {
+ int tid = getThreadId(t);
+ if (tid == threadId) {
+ return t;
+ }
+ }
+ return manager.getCurrentThread();
+ }
+
+ private TraceBreakpointKindSet kind(String mode) {
+ return mode.equals("hardware") ? CommonSet.HWX.kinds() : CommonSet.SWX.kinds();
+ }
+
+ private TraceBreakpointKindSet kinds(DataBreakpointAccessType accessType) {
+ return switch (accessType) {
+ case DataBreakpointAccessType.READ -> CommonSet.READ.kinds();
+ case DataBreakpointAccessType.WRITE -> CommonSet.WRITE.kinds();
+ case DataBreakpointAccessType.READ_WRITE -> CommonSet.ACCESS.kinds();
+ case null -> CommonSet.SWX.kinds();
+ default -> CommonSet.SWX.kinds();
+ };
+ }
+
+ List getBreakpointList() {
+ List rbpts = new ArrayList<>();
+ if (trace == null) {
+ return rbpts;
+ }
+
+ Collection extends TraceBreakpointLocation> bpts =
+ trace.getBreakpointManager().getAllBreakpointLocations();
+ for (TraceBreakpointLocation loc : bpts) {
+ AddressRange range = loc.getRange(manager.getCurrentSnap());
+ if (range == null) {
+ continue;
+ }
+ String key = loc.getName(manager.getCurrentSnap());
+ key = key.substring(1, key.indexOf("]"));
+
+ Breakpoint rbpt = new Breakpoint();
+ rbpt.setId(Integer.parseInt(key));
+ rbpt.setInstructionReference(range.getMinAddress().toString(false));
+ //rbpt.setOffset(0);
+ rbpt.setVerified(true);
+ rbpts.add(rbpt);
+ }
+ return rbpts;
+ }
+
+ private Address getAddress(String offset) {
+ if (offset == null || space == null) {
+ return null;
+ }
+
+ try {
+ return space.getAddress(offset);
+ }
+ catch (AddressFormatException e) {
+ return null;
+ }
+ }
+
+ private String getDisplay(TraceObject obj) {
+ if (obj == null) {
+ return "UNKNOWN";
+ }
+ var displayAttr = obj.getValue(manager.getCurrentSnap(), TraceObjectInterface.KEY_DISPLAY);
+ if (displayAttr == null || displayAttr.getValue() == null) {
+ return "UNKNOWN";
+ }
+ Object display = displayAttr.getValue();
+ if (display instanceof String desc) {
+ return desc;
+ }
+ return display.toString();
+ }
+
+// private int getLine(Address address) {
+// SourceFileManager sourceManager = plugin.getCurrentProgram().getSourceFileManager();
+// List sourceMapEntries = sourceManager.getSourceMapEntries(address);
+// for (SourceMapEntry entry : sourceMapEntries) {
+// return entry.getLineNumber();
+// }
+// return -1;
+// }
+//
+// private String getFile(Address address) {
+// SourceFileManager sourceManager = plugin.getCurrentProgram().getSourceFileManager();
+// List sourceMapEntries = sourceManager.getSourceMapEntries(address);
+// for (SourceMapEntry entry : sourceMapEntries) {
+// return entry.getSourceFile().getFilename();
+// }
+// return "";
+// }
+
+ public void setClient(IDebugProtocolClient client) {
+ this.client = client;
+
+ StoppedEventArguments args = new StoppedEventArguments();
+ args.setAllThreadsStopped(true);
+ args.setReason("initialization");
+ client.stopped(args);
+ }
+
+ public IDebugProtocolClient getClient() {
+ return this.client;
+ }
+
+ // UNSUPPORTED but called enough we'd rather not throw an exception
+
+ @Override
+ public CompletableFuture configurationDone(ConfigurationDoneArguments args) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ @Override
+ public CompletableFuture scopes(ScopesArguments args) {
+ ScopesResponse response = new ScopesResponse();
+ return CompletableFuture.completedFuture(response);
+ }
+
+ @Override
+ public CompletableFuture variables(VariablesArguments args) {
+ return CompletableFuture.completedFuture(null);
+ }
+}
diff --git a/Ghidra/Debug/Debugger-dap/src/main/java/dap/DapPlugin.java b/Ghidra/Debug/Debugger-dap/src/main/java/dap/DapPlugin.java
new file mode 100644
index 0000000000..fb6dcd3870
--- /dev/null
+++ b/Ghidra/Debug/Debugger-dap/src/main/java/dap/DapPlugin.java
@@ -0,0 +1,377 @@
+/* ###
+ * 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 dap;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.*;
+
+import org.eclipse.lsp4j.debug.*;
+import org.eclipse.lsp4j.debug.Module;
+import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
+
+import docking.action.builder.ActionBuilder;
+import ghidra.app.CorePluginPackage;
+import ghidra.app.events.ProgramActivatedPluginEvent;
+import ghidra.app.events.ProgramClosedPluginEvent;
+import ghidra.app.plugin.PluginCategoryNames;
+import ghidra.app.plugin.ProgramPlugin;
+import ghidra.app.plugin.core.debug.event.*;
+import ghidra.app.services.DebuggerTraceManagerService;
+import ghidra.debug.api.target.Target;
+import ghidra.debug.api.tracemgr.DebuggerCoordinates;
+import ghidra.framework.model.DomainObjectChangeRecord;
+import ghidra.framework.model.DomainObjectEvent;
+import ghidra.framework.options.OptionType;
+import ghidra.framework.options.ToolOptions;
+import ghidra.framework.plugintool.*;
+import ghidra.framework.plugintool.util.PluginStatus;
+import ghidra.program.database.SpecExtension;
+import ghidra.program.model.address.AddressRange;
+import ghidra.program.model.address.AddressSpace;
+import ghidra.program.model.listing.Program;
+import ghidra.trace.model.*;
+import ghidra.trace.model.breakpoint.TraceBreakpointLocation;
+import ghidra.trace.model.modules.TraceModule;
+import ghidra.trace.model.target.TraceObjectValue;
+import ghidra.trace.model.thread.TraceProcess;
+import ghidra.trace.model.thread.TraceThread;
+import ghidra.trace.model.time.TraceSnapshot;
+import ghidra.trace.util.TraceEvents;
+import ghidra.util.HelpLocation;
+
+//@formatter:off
+@PluginInfo(
+ status = PluginStatus.UNSTABLE,
+ packageName = CorePluginPackage.NAME,
+ category = PluginCategoryNames.ANALYSIS,
+ shortDescription = "DapDebuggerSupport",
+ description = "Plugin implements server-side DAP access to the debugger",
+ eventsConsumed = {
+ ProgramActivatedPluginEvent.class, ProgramClosedPluginEvent.class,
+ TraceActivatedPluginEvent.class, TraceOpenedPluginEvent.class,
+ TraceClosedPluginEvent.class
+ })
+//@formatter:on
+
+public class DapPlugin extends ProgramPlugin {
+
+ public final static String HELP_LOCATION = "dap";
+ private static final String OPTIONS_TITLE = "DAP Server";
+
+ private DapServer server;
+ private final Map listeners = new HashMap<>();
+ private IDebugProtocolClient client;
+
+ public DapPlugin(PluginTool tool) {
+ super(tool);
+ createActions();
+ ToolOptions opt = tool.getOptions(OPTIONS_TITLE);
+ opt.registerOption("Port", OptionType.INT_TYPE, 54321, null, "Port to connect to");
+ opt.registerOption("IP address", OptionType.STRING_TYPE,
+ InetAddress.getLoopbackAddress().getHostAddress(), null,
+ "IP address to connect to");
+ }
+
+ private void createActions() {
+
+ new ActionBuilder("Start DAP Server", getName())
+ .menuPath("Debugger", "DAP", "Start server")
+ .helpLocation(new HelpLocation(getName(),HELP_LOCATION))
+ .enabledWhen(_ -> currentProgram != null)
+ .onAction(_ -> doStartServer())
+ .buildAndInstall(tool);
+
+
+ new ActionBuilder("Stop DAP Server", getName())
+ .menuPath("Debugger", "DAP", "Stop server")
+ .helpLocation(new HelpLocation(getName(), HELP_LOCATION))
+ .enabledWhen(_ -> currentProgram != null)
+ .onAction(_ -> doStopServer())
+ .buildAndInstall(tool);
+ }
+
+ private void doStartServer() {
+ DapDebugAdapter adapter = new DapDebugAdapter(DapPlugin.this, tool);
+ server = new DapServer(DapPlugin.this, adapter);
+ server.startServer();
+ }
+
+ private void doStopServer() {
+ if (server != null) {
+ server.stopServer();
+ server = null;
+ }
+
+ var traceIterator = listeners.keySet().iterator();
+ while (traceIterator.hasNext()) {
+ Trace trace = traceIterator.next();
+ ListenerForChanges listener = listeners.get(trace);
+ if (listener != null) {
+ trace.removeListener(listener);
+ }
+ traceIterator.remove();
+ }
+ }
+
+ @Override
+ public void processEvent(PluginEvent event) {
+ super.processEvent(event);
+
+ if (event instanceof ProgramClosedPluginEvent closedProgEvent) {
+ Program program = closedProgEvent.getProgram();
+ if (currentProgram != null && currentProgram.equals(program)) {
+ currentProgram = null;
+ }
+ return;
+ }
+
+ if (event instanceof ProgramActivatedPluginEvent activeProgEvent) {
+ currentProgram = activeProgEvent.getActiveProgram();
+ if (currentProgram != null) {
+ SpecExtension.registerOptions(currentProgram);
+ }
+ return;
+ }
+
+ if (event instanceof TraceClosedPluginEvent closedEvent) {
+ removeListener(closedEvent.getTrace());
+ return;
+ }
+
+ if (event instanceof TraceActivatedPluginEvent activeTraceEvent) {
+ if (server == null) {
+ return;
+ }
+ server.updateAdapter(activeTraceEvent.getActiveCoordinates());
+ return;
+ }
+
+ }
+
+ void addListener(Trace trace) {
+ if (trace == null || listeners.containsKey(trace)) {
+ return;
+ }
+
+ ListenerForChanges listener = new ListenerForChanges(trace);
+ trace.addListener(listener);
+ listeners.put(trace, listener);
+ }
+
+ void removeListener(Trace trace) {
+ if (trace == null) {
+ return;
+ }
+ ListenerForChanges listener = listeners.remove(trace);
+ if (listener != null) {
+ trace.removeListener(listener);
+ }
+ }
+
+ protected class ListenerForChanges extends TraceDomainObjectListener {
+
+ private final Trace trace;
+ private DebuggerTraceManagerService manager;
+
+ public ListenerForChanges(Trace trace) {
+ this.trace = trace;
+ this.manager = tool.getService(DebuggerTraceManagerService.class);
+ listenForUntyped(DomainObjectEvent.RESTORED, this::objectRestored);
+ listenForUntyped(DomainObjectEvent.CLOSED, this::objectClosed);
+ listenFor(TraceEvents.VALUE_CREATED, this::valueModified);
+ listenFor(TraceEvents.VALUE_DELETED, this::valueModified);
+ listenFor(TraceEvents.VALUE_LIFESPAN_CHANGED, this::valueModified);
+ listenFor(TraceEvents.THREAD_ADDED, this::threadChanged);
+ listenFor(TraceEvents.THREAD_DELETED, this::threadChanged);
+ listenFor(TraceEvents.THREAD_LIFESPAN_CHANGED, this::threadChanged);
+ listenFor(TraceEvents.BREAKPOINT_ADDED, this::breakpointChanged);
+ listenFor(TraceEvents.BREAKPOINT_CHANGED, this::breakpointChanged);
+ listenFor(TraceEvents.BREAKPOINT_DELETED, this::breakpointChanged);
+ listenFor(TraceEvents.BREAKPOINT_LIFESPAN_CHANGED, this::breakpointChanged);
+ listenFor(TraceEvents.MODULE_ADDED, this::moduleAdded);
+ listenFor(TraceEvents.MODULE_CHANGED, this::moduleChanged);
+ listenFor(TraceEvents.MODULE_DELETED, this::moduleDeleted);
+ listenFor(TraceEvents.MODULE_LIFESPAN_CHANGED, this::moduleChanged);
+ listenFor(TraceEvents.BYTES_CHANGED, this::bytesChanged);
+ listenFor(TraceEvents.PLATFORM_DELETED, this::objectClosed);
+ }
+
+ private void objectRestored(DomainObjectChangeRecord record) {
+ contextChanged();
+ }
+
+ private void objectClosed(DomainObjectChangeRecord record) {
+ ExitedEventArguments args = new ExitedEventArguments();
+ TraceProcess process = server.getAdapter().getProcess();
+
+ var exitAttr = process.getObject().getAttribute(manager.getCurrentSnap(), "Exit Code");
+ if (exitAttr != null && exitAttr.getValue() != null) {
+ Long rc = (Long) exitAttr.getValue();
+ args.setExitCode(rc.intValue());
+ }
+ else {
+ args.setExitCode(-1);
+ }
+ client.exited(args);
+ }
+
+ protected void valueModified(TraceObjectValue value) {
+ contextChanged();
+ }
+
+ private void threadChanged(TraceThread thread) {
+ contextChanged();
+ }
+
+ private void breakpointChanged(TraceBreakpointLocation location) {
+ contextChanged();
+ }
+
+ private void moduleAdded(TraceModule module) {
+ moduleUpdated(module, ModuleEventArgumentsReason.NEW);
+ }
+
+ private void moduleDeleted(TraceModule module) {
+ moduleUpdated(module, ModuleEventArgumentsReason.REMOVED);
+ }
+
+ private void moduleChanged(TraceModule module) {
+ moduleUpdated(module, ModuleEventArgumentsReason.CHANGED);
+ }
+
+ private void bytesChanged(AddressSpace space, TraceAddressSnapRange range) {
+ MemoryEventArguments args = new MemoryEventArguments();
+ AddressRange addrRange = range.getRange();
+ String ref = addrRange.getMinAddress().toString(false);
+ args.setCount((int) addrRange.getLength());
+ args.setOffset(0);
+ args.setMemoryReference(ref);
+ client.memory(args);
+ }
+
+ private void contextChanged() {
+ if (client == null) {
+ return;
+ }
+ DebuggerCoordinates coordinates = manager.getCurrentFor(trace);
+ Target target = coordinates.getTarget();
+ long currentSnap = manager.getCurrentSnap();
+
+ for (TraceThread t : trace.getThreadManager().getLiveThreads(currentSnap)) {
+ int tid = server.getAdapter().getThreadId(t);
+ TraceExecutionState state = target.getThreadExecutionState(t);
+ switch (state) {
+ case STOPPED -> {
+ StoppedEventArguments args = new StoppedEventArguments();
+ args.setThreadId(tid);
+ TraceSnapshot snapshot =
+ trace.getTimeManager().getSnapshot(currentSnap, false);
+ args.setDescription(
+ snapshot == null ? "STOPPED" : snapshot.getDescription());
+ args.setReason(ThreadEventArgumentsReason.STARTED);
+ client.stopped(args);
+ }
+ case RUNNING -> {
+ ContinuedEventArguments args = new ContinuedEventArguments();
+ args.setThreadId(tid);
+ client.continued(args);
+ }
+ case ALIVE, INACTIVE -> {
+ ThreadEventArguments args = new ThreadEventArguments();
+ args.setThreadId(tid);
+ args.setReason(ThreadEventArgumentsReason.STARTED);
+ client.thread(args);
+ }
+ case TERMINATED -> {
+ ThreadEventArguments args = new ThreadEventArguments();
+ args.setThreadId(tid);
+ args.setReason(ThreadEventArgumentsReason.EXITED);
+ client.thread(args);
+ }
+ }
+ }
+
+ // NOTE: Yes, this is a lot of probably-mostly-extranous work, but...
+ // right now, the alternatives seem grim.
+ List breakpointList = server.getAdapter().getBreakpointList();
+ for (Breakpoint bpt : breakpointList) {
+ BreakpointEventArguments args = new BreakpointEventArguments();
+ args.setBreakpoint(bpt);
+ client.breakpoint(args);
+ }
+
+ for (TraceModule mod : trace.getModuleManager()
+ .getLoadedModules(manager.getCurrentSnap())) {
+ moduleUpdated(mod, ModuleEventArgumentsReason.CHANGED);
+ }
+ }
+
+ private void moduleUpdated(TraceModule module, ModuleEventArgumentsReason reason) {
+ ModuleEventArguments args = new ModuleEventArguments();
+ Module rmod = new Module();
+ long currentSnap = manager.getCurrentSnap();
+
+ rmod.setId((int) module.getObject().getKey());
+ rmod.setName(module.getName(currentSnap));
+
+ var range = module.getRange(currentSnap);
+ rmod.setAddressRange(range != null ? range.toString() : "Unknown");
+
+ args.setModule(rmod);
+ args.setReason(reason);
+ client.module(args);
+ }
+
+ }
+
+ public void setClient(IDebugProtocolClient client) {
+ this.client = client;
+ DebuggerTraceManagerService manager =
+ tool.getService(DebuggerTraceManagerService.class);
+ if (manager != null) {
+ Trace currentTrace = manager.getCurrentTrace();
+ if (currentTrace != null) {
+ addListener(currentTrace);
+ }
+ }
+ }
+
+ public IDebugProtocolClient getClient() {
+ return this.client;
+ }
+
+ public Integer getPort() {
+ ToolOptions opt = tool.getOptions(OPTIONS_TITLE);
+ return opt.getInt("Port", 54321);
+ }
+
+ public InetAddress getInetAddress() {
+ ToolOptions opt = tool.getOptions(OPTIONS_TITLE);
+ String addr =
+ opt.getString("IP address", InetAddress.getLoopbackAddress().getHostAddress());
+ if (addr == null || addr.isEmpty() || addr.equals("localhost")) {
+ return InetAddress.getLoopbackAddress();
+ }
+ try {
+ return InetAddress.getByName(addr);
+ }
+ catch (UnknownHostException e) {
+ return InetAddress.getLoopbackAddress();
+ }
+ }
+
+}
diff --git a/Ghidra/Debug/Debugger-dap/src/main/java/dap/DapServer.java b/Ghidra/Debug/Debugger-dap/src/main/java/dap/DapServer.java
new file mode 100644
index 0000000000..ac71804bbb
--- /dev/null
+++ b/Ghidra/Debug/Debugger-dap/src/main/java/dap/DapServer.java
@@ -0,0 +1,155 @@
+/* ###
+ * 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 dap;
+
+import java.io.*;
+import java.net.*;
+import java.util.concurrent.*;
+
+import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
+import org.eclipse.lsp4j.jsonrpc.Launcher;
+import org.eclipse.lsp4j.jsonrpc.debug.DebugLauncher;
+
+import ghidra.debug.api.tracemgr.DebuggerCoordinates;
+import ghidra.util.Msg;
+
+public class DapServer implements Runnable {
+
+ private DapPlugin plugin;
+ private DapDebugAdapter adapter;
+ private InetAddress address;
+ private int port;
+
+ private ServerSocket server;
+ private Thread listenerThread;
+ private ExecutorService sessionThreadPool = Executors.newCachedThreadPool();
+ private volatile boolean running = false;
+
+ public DapServer(DapPlugin plugin, DapDebugAdapter adapter) {
+ this.plugin = plugin;
+ this.adapter = adapter;
+ this.address = plugin.getInetAddress();
+ this.port = plugin.getPort();
+ }
+
+ public void startServer() {
+ if (running) {
+ return;
+ }
+ try {
+ if (server != null && !server.isClosed()) {
+ server.close();
+ }
+ server = new ServerSocket(port, 50, address);
+ this.running = true;
+
+ this.listenerThread = new Thread(this, "DAP-Server-Listener");
+ this.listenerThread.start();
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Could not start server" + e.getMessage());
+ }
+ }
+
+ public void stopServer() {
+ if (!running) {
+ return;
+ }
+ running = false;
+
+ if (server != null) {
+ try {
+ if (!server.isClosed()) {
+ server.close();
+ }
+ }
+ catch (IOException e) {
+ Msg.warn(this, "Error closing server socket: " + e.getMessage());
+ }
+ server = null;
+ }
+
+ if (listenerThread != null) {
+ listenerThread.interrupt();
+ listenerThread = null;
+ }
+
+ if (sessionThreadPool != null) {
+ sessionThreadPool.shutdownNow();
+ try {
+ if (!sessionThreadPool.awaitTermination(2, TimeUnit.SECONDS)) {
+ Msg.warn(this, "Session thread pool did not terminate cleanly.");
+ }
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ sessionThreadPool = null;
+ }
+ }
+
+ @Override
+ public void run() {
+ ServerSocket localServer = this.server;
+ while (running && localServer != null && !localServer.isClosed()) {
+ try {
+ Socket clientSocket = localServer.accept();
+ if (running && sessionThreadPool != null) {
+ sessionThreadPool.submit(() -> handleClientSession(clientSocket));
+ }
+ else {
+ clientSocket.close(); // Safeguard against trailing boundary race connections
+ }
+ }
+ catch (IOException e) {
+ if (running) {
+ Msg.error(this, "Error accepting connection: " + e.getMessage());
+ }
+ }
+ }
+ }
+
+ private void handleClientSession(Socket socket) {
+ try (Socket s = socket;
+ InputStream in = s.getInputStream();
+ OutputStream out = s.getOutputStream()) {
+
+ Launcher launcher = DebugLauncher.createLauncher(
+ adapter,
+ IDebugProtocolClient.class,
+ in,
+ out);
+ IDebugProtocolClient client = launcher.getRemoteProxy();
+
+ adapter.setClient(client);
+ plugin.setClient(client);
+ launcher.startListening().get();
+
+ }
+ catch (Exception e) {
+ Msg.error(this, "Session error: " + e.getMessage());
+ }
+ }
+
+ public DapDebugAdapter getAdapter() {
+ return adapter;
+ }
+
+ public void updateAdapter(DebuggerCoordinates debuggerCoordinates) {
+ adapter.setCoordinates(debuggerCoordinates);
+ }
+
+}