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:

+ + + + 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 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 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 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); + } + +}