diff --git a/.gitignore b/.gitignore index 0b22c38..7705b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ mono-debug.userprefs npm-debug.log .vs/ .DS_Store +log.txt +log diff --git a/README.md b/README.md index 79a46d7..5528f4a 100644 --- a/README.md +++ b/README.md @@ -4,30 +4,19 @@ Unity Debug Adapter (DA) for debugging the Unity Editor or applications using th backend. > [!IMPORTANT] -> I am currently re-writing this project. There are a couple of issues and using dotnet rather -> than Mono to run this seems problematic. +> debugging IL2CPP applications is not and will not be supported. -> [!IMPORTANT] -> debugging IL2CPP applications is not supported. - -This project is adjusted (somewhat forked) from the deprecated and quite frankly bloated -[vscode-unity-debug][vscode-unity-debug] project. [vscode-unity-debug][vscode-unity-debug] does -not work out-of-the-box with new dotnet because of failure to detect the '\r\n\r\n' sequence -in client <-> debug-adapter messages. The failure is caused by an IndexOf("\r\n\r\n") issue -(see https://github.com/dotnet/runtime/issues/43736). - -Since the project is stale and no longer accepts pull-requests/patches, fixing -issues of the original [vscode-unity-debug][vscode-unity-debug] project and debloating -it are the reasons for the existence of this project. +This project is adjusted (forked) from the deprecated and *quite frankly* bloated +[vscode-unity-debug][vscode-unity-debug] project. -Hopefully when Unity finally moves to .NET Core, the need for this repository will cease to -exist. In the meantime, if you are doing Unity development on a text-editor/IDE other than -VSCode, Ryder, or Visual Studio, and you want debugging functionalities with a clear license -(MIT) then this project is for you. +If you are doing Unity development on a text-editor/IDE other than VSCode, +Ryder, or Visual Studio, and you want debugging functionalities with a +permissive license (MIT) then this project is for you. -In case you are looking for instructions on how to hook this to Neovim, see [neovim-unity][unity-debugger-support]. +In case you are looking for instructions on how to hook this to Neovim, see +[neovim-unity][unity-debugger-support]. -## Installation +## Build from Source Clone the repo and its submodule(s): @@ -64,7 +53,12 @@ Then, if you want to run the debug adapter: bin/Release/unity-debug-adapter.exe ``` -You should then be seeying an output like this: + If you built this from source, you might need to: + ```bash + chmod +x bin/Release/unity-debug-adapter.exe + ``` + +You should then get an output like this: ```text 21/08/2025 00:31:01 [I] waiting for debug protocol on stdin/stdout @@ -72,19 +66,80 @@ You should then be seeying an output like this: 21/08/2025 00:31:01 [I] done constructing UnityDebugSession ``` -## Usage +## For Developers -`unity-debug-adapter.exe` accepts two optional long parameters: -- `--trace-level` sets the logging trace level: `trace` | `debug` | `info` | `warn` | `error` | `critical` | `none` -- `--log-file` provides a path to a log file. In case this is not provided, and `--trace-level` is not `none`, logging - is output to stderr. +This section is mainly for developers interested in contributing or want to +learn the intenals of this project. -Example of an invocation: +### Overview -```bash -unity-debug-adapter.exe --trace-level=trace --log-file=dap-log.txt ``` + Translates `requests` from nvim (which are DAP conformant) + to Mono.Debugger-sepecific requests. + Translates Mono.Debugger-specific + responses to DAP-conformant `responses`. + Writes logs to s_LogFile or stderr Locally running Unity Editor (which always uses Mono). Or + | a local/remote running Unity Player instance using Mono + | backend (with debugging enabled) + | | + +------+ +-----------+ +--------------------+ < - - - - - + + | Nvim |----------- | UNITY DAP | ---------------- | UNITY | + +------+ ^ +-----------+ ^ | (Mono.Debugger) | + | | +--------------------+ + | | + via stdin and stdout + via a TCP/IP socket (ip:port) + (_outputStream and inputStream) +``` + +### Backends + +This debug adapter essentially communicates with the following backends: + +- Mono.Debugger.Soft: this is the official Mono debugger in `debugger-libs`. +- GDB: Mono applications can be debugged using `gdb`. This is still not +implemented yet and is TODO. + + +### Why not IL2CPP + +I will not include add support for debugging C# -> IL2CPP code mainly because +of these facts/opinions: + +- IL2CPP is closed source and I might get into trouble if I implement something +like what Visual Studio does (i.e., some sort of mapping between original C# +code and generated IL2CPP C++ code). +- I think it makes little sense to debug C++ code (generated or not) via +stepping through a completely different language (e.g., managed C#). +- Complexity. There are very few people who are using Neovim for Unity, fewer +are using debuggers, and even fewer who want to debug IL2CPP through C# via +Neovim. This is simply not worth the effort. +- IL2CPP is simply C++. Just debug it using a proper C++ debugger (e.g., gdb). + +### Why Not Use [vscode-unity-debug][vscode-unity-debug]? + +[vscode-unity-debug][vscode-unity-debug] does not work out-of-the-box with new +dotnet because of failure to detect the '\r\n\r\n' sequence in +client <-> debug-adapter messages. The failure is caused by an +`IndexOf("\r\n\r\n")` issue (see https://github.com/dotnet/runtime/issues/43736). + +Since the project is stale and no longer accepts pull-requests/patches, fixing +issues of the original [vscode-unity-debug][vscode-unity-debug] project and +debloating it are the reasons for the existence of this project. + +The project is also very poorly written, all responses are sent twice, the +project relies on heavy usage of the `dynamic` keyword which requires JIT and +which causes issues when this project is compiled with dotnet (rather than +xbuild). + + +### In an Ideal World + +Hopefully we get Unity .NET CLR support before the sun explodes so that we can +use actual proper, industry-standard, open-source, and actively maintained +.NET debuggers. +When that happens, this adapter will add support to it and will, probably, be +much more useful. ## License diff --git a/unity-debug-adapter/DebugSession.cs b/unity-debug-adapter/DebugSession.cs index d8751ff..f36ecd9 100644 --- a/unity-debug-adapter/DebugSession.cs +++ b/unity-debug-adapter/DebugSession.cs @@ -1,476 +1,133 @@ -#pragma warning disable IDE1006, IDE0003 - -using System; +using System; using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; +using System.Text.RegularExpressions; +using Newtonsoft.Json.Linq; namespace UnityDebugAdapter { - // ---- Types ------------------------------------------------------------------------- - - public class Message - { - public int id { get; } - public string format { get; } - public dynamic variables { get; } - public dynamic showUser { get; } - public dynamic sendTelemetry { get; } - - public Message(int id, string format, dynamic variables = null, bool user = true, bool telemetry = false) - { - this.id = id; - this.format = format; - this.variables = variables; - this.showUser = user; - this.sendTelemetry = telemetry; - } - } - - public class StackFrame - { - public int id { get; } - public Source source { get; } - public int line { get; } - public int column { get; } - public string name { get; } - public string presentationHint { get; } - - public StackFrame(int id, string name, Source source, int line, int column, string hint) - { - this.id = id; - this.name = name; - this.source = source; - - // These should NEVER be negative - this.line = Math.Max(0, line); - this.column = Math.Max(0, column); - - this.presentationHint = hint; - } - } - - public class Scope - { - public string name { get; } - public int variablesReference { get; } - public bool expensive { get; } - - public Scope(string name, int variablesReference, bool expensive = false) - { - this.name = name; - this.variablesReference = variablesReference; - this.expensive = expensive; - } - } - - public class Variable - { - public string name { get; } - public string value { get; } - public string type { get; } - public int variablesReference { get; } - - public Variable(string name, string value, string type, int variablesReference = 0) - { - this.name = name; - this.value = value; - this.type = type; - this.variablesReference = variablesReference; - } - } - - public class Thread - { - public int id { get; } - public string name { get; } - - public Thread(int id, string name) - { - this.id = id; - if (name == null || name.Length == 0) - { - this.name = string.Format("Thread #{0}", id); - } - else - { - this.name = name; - } - } - } - - public class Source - { - public string name { get; } - public string path { get; } - public int sourceReference { get; } - public string presentationHint { get; } - - public Source(string name, string path, int sourceReference, string hint) - { - this.name = name; - this.path = path; - this.sourceReference = sourceReference; - this.presentationHint = hint; - } - } - - public class Breakpoint - { - public int id { get; } - public bool verified { get; } - public string message { get; } - public Source source { get; } - public int line { get; } - public int column { get; } - public int endLine { get; } - public int endColumn { get; } - - public Breakpoint(bool verified, int line, int column, string logMessage) - { - this.verified = verified; - this.line = line; - this.column = column; - this.message = logMessage; - } - } - - // ---- Events ------------------------------------------------------------------------- - - public class InitializedEvent : Event - { - public InitializedEvent() - : base("initialized") { } - } - - public class StoppedEvent : Event - { - public StoppedEvent(int tid, string reasn, string txt = null) - : base("stopped", new - { - threadId = tid, - reason = reasn, - text = txt, - allThreadsStopped = true - }) - { } - } - - public class ExitedEvent : Event - { - public ExitedEvent(int exCode) - : base("exited", new { exitCode = exCode }) { } - } - - public class TerminatedEvent : Event - { - public TerminatedEvent() - : base("terminated") { } - } - - public class ThreadEvent : Event - { - public ThreadEvent(string reasn, int tid) - : base("thread", new - { - reason = reasn, - threadId = tid - }) - { } - } - - public class OutputEvent : Event - { - public OutputEvent(string cat, string outpt) - : base("output", new - { - category = cat, - output = outpt - }) - { } - } - - // ---- Response ------------------------------------------------------------------------- - - public class Capabilities : ResponseBody - { - public bool supportsConfigurationDoneRequest; - public bool supportsFunctionBreakpoints; - public bool supportsConditionalBreakpoints; - public bool supportsEvaluateForHovers; - public bool supportsSetVariable; - public bool supportsHitConditionalBreakpoints; - public bool supportsExceptionOptions; - public bool supportsLogPoints; - public ExceptionBreakpointsFilter[] exceptionBreakpointFilters; - } - - public class ExceptionBreakpointsFilter - { - public string filter { get; } - public string label { get; } - - [JsonProperty("default")] - public bool? defaultValue { get; } - - public ExceptionBreakpointsFilter(string filter, string label, bool defaultValue = false) - { - this.filter = filter; - this.label = label; - this.defaultValue = defaultValue; - } - } - - public class ErrorResponseBody : ResponseBody - { - public Message error { get; } - - public ErrorResponseBody(Message error) - { - this.error = error; - } - } - - public class StackTraceResponseBody : ResponseBody - { - public StackFrame[] stackFrames { get; } - public int totalFrames { get; } - - public StackTraceResponseBody(List frames, int total) - { - stackFrames = frames.ToArray(); - totalFrames = total; - } - } - - public class ScopesResponseBody : ResponseBody - { - public Scope[] scopes { get; } - - public ScopesResponseBody(List scps) - { - scopes = scps.ToArray(); - } - } - - public class VariablesResponseBody : ResponseBody - { - public Variable[] variables { get; } - - public VariablesResponseBody(List vars) - { - variables = vars.ToArray(); - } - } - - public class ThreadsResponseBody : ResponseBody - { - public Thread[] threads { get; } - - public ThreadsResponseBody(List ths) - { - threads = ths.ToArray(); - } - } - - public class EvaluateResponseBody : ResponseBody - { - public string result { get; } - public int variablesReference { get; } - - public EvaluateResponseBody(string value, int reff = 0) - { - result = value; - variablesReference = reff; - } - } - - public class SetBreakpointsResponseBody : ResponseBody - { - public Breakpoint[] breakpoints { get; } - - public SetBreakpointsResponseBody(List bpts = null) - { - if (bpts == null) - breakpoints = new Breakpoint[0]; - else - breakpoints = bpts.ToArray(); - } - } - - public class SetVariablesResponseBody : ResponseBody - { - public string value { get; } - public string type { get; } - public int variablesReference { get; } - - public SetVariablesResponseBody(string value, string type, int variablesReference) - { - this.value = value; - this.type = type; - this.variablesReference = variablesReference; - } - } - - public class ContinueResponseBody : ResponseBody - { - public bool allThreadsContinued = true; - } - - public class SetFunctionBreakpointsBody : ResponseBody - { - public Breakpoint[] breakpoints { get; } - - public SetFunctionBreakpointsBody(Breakpoint[] breakpoints) - { - this.breakpoints = breakpoints; - } - } - - // ---- The Session -------------------------------------------------------- - public abstract class DebugSession : ProtocolServer { - private bool _clientLinesStartAt1 = true; - private bool _clientPathsAreURI = true; + protected static readonly Regex VARIABLE_REGEX = new Regex(@"\{_(\w+)\}"); + protected bool _clientLinesStartAt1 = true; + protected bool _clientPathsAreURI = true; public DebugSession() { } - public void SendResponse(Response response, dynamic body = null) + public void SendErrorResponse(int requestSequence, string command, int id, string format, + Dictionary variables = null, bool user = true, bool telemetry = false) { - if (body != null) + format ??= ""; + variables ??= new Dictionary(); + var response = new Response() { - response.SetBody(body); - } - - SendMessage(response); - } + command = command, + request_seq = requestSequence, + success = false, // this is set in SetErrorBody (but also just set it here for readability) + }; + var msg = new Message(id, format, variables, user, telemetry); + string msg_str = VARIABLE_REGEX.Replace(format, m => + { + if (variables.TryGetValue(m.Groups[1].Value, out string replacement)) + return replacement; + return $"{{{m.Groups[1].Value}}}: not found"; + }); - public void SendErrorResponse(Response response, int id, string format, dynamic arguments = null, bool user = true, bool telemetry = false) - { - var msg = new Message(id, format, arguments, user, telemetry); - var message = Utilities.ExpandVariables(msg.format, msg.variables); - response.SetErrorBody(message, new ErrorResponseBody(msg)); + response.SetErrorBody(msg_str, new ErrorResponseBody(msg)); SendMessage(response); } - protected override void DispatchRequest(string command, dynamic args, Response response) + protected override void DispatchRequest(int reqSeq, string command, JToken args) { - if (args == null) - { - args = new { }; - } - try { switch (command) { case "initialize": - if (args.linesStartAt1 != null) - { - _clientLinesStartAt1 = (bool)args.linesStartAt1; - } - - var pathFormat = (string)args.pathFormat; - if (pathFormat != null) - { - switch (pathFormat) - { - case "uri": - _clientPathsAreURI = true; - break; - case "path": - _clientPathsAreURI = false; - break; - default: - SendErrorResponse(response, 1015, "initialize: bad value '{_format}' for pathFormat", new { _format = pathFormat }); - return; - } - } - - Initialize(response, args); + Initialize(reqSeq, args); break; case "launch": - Launch(response, args); + Launch(reqSeq, args); break; case "attach": - Attach(response, args); + Attach(reqSeq, args); break; case "disconnect": - Disconnect(response, args); + Disconnect(reqSeq, args); break; case "next": - Next(response, args); + Next(reqSeq, args); break; case "continue": - Continue(response, args); + Continue(reqSeq, args); break; case "stepIn": - StepIn(response, args); + StepIn(reqSeq, args); break; case "stepOut": - StepOut(response, args); + StepOut(reqSeq, args); break; case "pause": - Pause(response, args); + Pause(reqSeq, args); break; case "stackTrace": - StackTrace(response, args); + StackTrace(reqSeq, args); break; case "scopes": - Scopes(response, args); + Scopes(reqSeq, args); break; case "variables": - Variables(response, args); + Variables(reqSeq, args); break; case "source": - Source(response, args); + Source(reqSeq, args); break; case "threads": - Threads(response, args); + Threads(reqSeq, args); break; case "setBreakpoints": - SetBreakpoints(response, args); + SetBreakpoints(reqSeq, args); break; case "setFunctionBreakpoints": - SetFunctionBreakpoints(response, args); + SetFunctionBreakpoints(reqSeq, args); break; case "setExceptionBreakpoints": - SetExceptionBreakpoints(response, args); + SetExceptionBreakpoints(reqSeq, args); break; case "evaluate": - Evaluate(response, args); + Evaluate(reqSeq, args); break; case "setVariable": - SetVariable(response, args); + SetVariable(reqSeq, args); break; default: - SendErrorResponse(response, 1014, "unrecognized request: {_request}", new { _request = command }); + SendErrorResponse(reqSeq, command, 1014, "unrecognized request: {_request}", + new Dictionary { { "_request", command } }); break; } } catch (Exception e) { - SendErrorResponse(response, 1104, "error while processing request '{_request}' (exception: {_exception})", new { _request = command, _exception = e.Message }); + SendErrorResponse(reqSeq, command, 1104, "error while processing request '{_request}' (exception: {_exception})", + new Dictionary { { "_request", command }, { "_exception", e.Message } }); } if (command == "disconnect") @@ -479,56 +136,50 @@ protected override void DispatchRequest(string command, dynamic args, Response r } } - protected abstract void SetVariable(Response response, object args); + protected abstract void SetVariable(int reqSeq, JToken args); - public abstract void Initialize(Response response, dynamic args); + public abstract void Initialize(int reqSeq, JToken args); - public abstract void Launch(Response response, dynamic arguments); + public abstract void Launch(int reqSeq, JToken args); - public abstract void Attach(Response response, dynamic arguments); + public abstract void Attach(int reqSeq, JToken args); - public abstract void Disconnect(Response response, dynamic arguments); + public abstract void Disconnect(int reqSeq, JToken args); - public abstract void SetFunctionBreakpoints(Response response, dynamic arguments); + public abstract void SetFunctionBreakpoints(int reqSeq, JToken args); - public abstract void SetExceptionBreakpoints(Response response, dynamic arguments); + public abstract void SetExceptionBreakpoints(int reqSeq, JToken args); - public abstract void SetBreakpoints(Response response, dynamic arguments); + public abstract void SetBreakpoints(int reqSeq, JToken args); - public abstract void Continue(Response response, dynamic arguments); + public abstract void Continue(int reqSeq, JToken args); - public abstract void Next(Response response, dynamic arguments); + public abstract void Next(int reqSeq, JToken args); - public abstract void StepIn(Response response, dynamic arguments); + public abstract void StepIn(int reqSeq, JToken args); - public abstract void StepOut(Response response, dynamic arguments); + public abstract void StepOut(int reqSeq, JToken args); - public abstract void Pause(Response response, dynamic arguments); + public abstract void Pause(int reqSeq, JToken args); - public abstract void StackTrace(Response response, dynamic arguments); + public abstract void StackTrace(int reqSeq, JToken args); - public abstract void Scopes(Response response, dynamic arguments); + public abstract void Scopes(int reqSeq, JToken args); - public abstract void Variables(Response response, dynamic arguments); + public abstract void Variables(int reqSeq, JToken args); - public abstract void Source(Response response, dynamic arguments); + public abstract void Source(int reqSeq, JToken args); - public abstract void Threads(Response response, dynamic arguments); + public abstract void Threads(int reqSeq, JToken args); - public abstract void Evaluate(Response response, dynamic arguments); + public abstract void Evaluate(int reqSeq, JToken args); - // protected protected int ConvertDebuggerLineToClient(int line) { return _clientLinesStartAt1 ? line : line - 1; } - protected int ConvertClientLineToDebugger(int line) - { - return _clientLinesStartAt1 ? line : line + 1; - } - protected string ConvertDebuggerPathToClient(string path) { if (_clientPathsAreURI) @@ -548,29 +199,5 @@ protected string ConvertDebuggerPathToClient(string path) return path; } } - - protected string ConvertClientPathToDebugger(string clientPath) - { - if (clientPath == null) - { - return null; - } - - if (_clientPathsAreURI) - { - if (Uri.IsWellFormedUriString(clientPath, UriKind.Absolute)) - { - Uri uri = new Uri(clientPath); - return uri.LocalPath; - } - - Logger.LogError($"path not well formed: '{clientPath}'"); - return null; - } - else - { - return clientPath; - } - } } } diff --git a/unity-debug-adapter/Logger.cs b/unity-debug-adapter/Logger.cs index c66e3c3..ff3cb29 100644 --- a/unity-debug-adapter/Logger.cs +++ b/unity-debug-adapter/Logger.cs @@ -28,69 +28,69 @@ public static void SetLogStream(TextWriter stream) public static void SetLogLevel(LogLevel logLevel) => s_LogLevel = logLevel; - public static void LogTrace(string msg) + public static void LogTrace(string msg, params object[] args) { if (s_LogLevel > LogLevel.TRACE) { return; } - s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [T] {msg}"); + s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [T] {string.Format(msg, args)}"); s_LogFile.Flush(); } - public static void LogDebug(string msg) + public static void LogDebug(string msg, params object[] args) { if (s_LogLevel > LogLevel.DEBUG) { return; } - s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [D] {msg}"); + s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [D] {string.Format(msg, args)}"); s_LogFile.Flush(); } - public static void LogInfo(string msg) + public static void LogInfo(string msg, params object[] args) { if (s_LogLevel > LogLevel.INFORMATION) { return; } - s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [I] {msg}"); + s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [I] {string.Format(msg, args)}"); s_LogFile.Flush(); } - public static void LogWarn(string msg) + public static void LogWarn(string msg, params object[] args) { if (s_LogLevel > LogLevel.WARNING) { return; } - s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [W] {msg}"); + s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [W] {string.Format(msg, args)}"); s_LogFile.Flush(); } - public static void LogError(string msg) + public static void LogError(string msg, params object[] args) { if (s_LogLevel > LogLevel.ERROR) { return; } - s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [E] {msg}"); + s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [E] {string.Format(msg, args)}"); s_LogFile.Flush(); } - public static void LogCritical(string msg) + public static void LogCritical(string msg, params object[] args) { if (s_LogLevel > LogLevel.CRITICAL) { return; } - s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [C] {msg}"); + s_LogFile.WriteLine($"{DateTime.Now:dd\\/MM\\/yyyy HH:mm:ss} [C] {string.Format(msg, args)}"); s_LogFile.Flush(); } diff --git a/unity-debug-adapter/UnityDebugAdapter.cs b/unity-debug-adapter/Program.cs similarity index 100% rename from unity-debug-adapter/UnityDebugAdapter.cs rename to unity-debug-adapter/Program.cs diff --git a/unity-debug-adapter/Protocol.cs b/unity-debug-adapter/Protocol.cs index be9a711..e751748 100644 --- a/unity-debug-adapter/Protocol.cs +++ b/unity-debug-adapter/Protocol.cs @@ -1,18 +1,80 @@ -#pragma warning disable IDE1006, IDE0003, IDE0038 +#pragma warning disable IDE1006, IDE0003 using System; -using System.Text; -using System.IO; -using System.Threading.Tasks; -using System.Text.RegularExpressions; -using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; namespace UnityDebugAdapter { + ////////////////////////////////////////////////////////////////////////////// + /// BASE PROTOCOL + ////////////////////////////////////////////////////////////////////////////// + + /// + /// On error (whenever success is false), the body can provide more details. + /// + public class ErrorResponseBody + { + /// + /// A structured error message. + /// + public Message error { get; } + + public ErrorResponseBody(Message error) + { + this.error = error; + } + } + + + /// + /// A debug adapter initiated event. + /// + public class Event : ProtocolMessage + { + /// + /// Type of event. + /// + [JsonProperty(PropertyName = "event")] + public string eventType { get; } + + /// + /// Event-specific information. + /// + public object body { get; } + + public Event(string type, object bdy = null) + : base("event") + { + eventType = type; + body = bdy; + } + } + + + /// + /// Base class of requests, responses, and events. + /// public class ProtocolMessage { + + /// + /// Sequence number of the message (also known as message ID). The `seq` for + /// the first message sent by a client or debug adapter is 1, and for each + /// subsequent message is 1 greater than the previous message sent by that + /// actor. `seq` can be used to order requests, responses, and events, and to + /// associate requests with their corresponding responses. For protocol + /// messages of type `request` the sequence number can be used to cancel the + /// request. + /// public int seq; + + + /// + /// Message type. + /// Values: 'request', 'response', 'event', etc. + /// public string type; public ProtocolMessage() { } @@ -27,23 +89,41 @@ public ProtocolMessage(string typ, int sq) type = typ; seq = sq; } + + public override string ToString() + { + var bodyJson = JsonConvert.SerializeObject(this); + // print rnrn instead of \r\n\r\n to avoid ugly log output + string header = string.Format($"Content-Length: {bodyJson.Length}rnrn"); + return header + bodyJson; + } } + /// + /// A client or debug adapter initiated request. + /// public class Request : ProtocolMessage { - public string command; - public dynamic arguments; + /// + /// The command to execute. + /// + public string command = null; + + /// + /// Object containing arguments for the command. + /// + public object arguments = new object(); public Request() { } - public Request(string cmd, dynamic arg) + public Request(string cmd, object arg) : base("request") { command = cmd; arguments = arg; } - public Request(int id, string cmd, dynamic arg) + public Request(int id, string cmd, object arg) : base("request", id) { command = cmd; @@ -51,34 +131,46 @@ public Request(int id, string cmd, dynamic arg) } } - /** Properties of a breakpoint or logpoint passed to the setBreakpoints request. */ - public class SourceBreakpoint - { - public int line; - public int column; - public string condition; - public string hitCondition; - public string logMessage; - } - - /* - * subclasses of ResponseBody are serialized as the body of a response. - * Don't change their instance variables since that will break the debug protocol. - */ - public class ResponseBody - { - // empty - } + /// + /// Response for a request. + /// public class Response : ProtocolMessage { + + /// + /// Sequence number of the corresponding request. + /// + public int request_seq; + + + /// + /// Outcome of the request. If true, the request was successful and the `body` attribute may contain + /// the result of the request. If the value is false, the attribute `message` contains the error in short + /// form and the `body` may contain additional information (see `ErrorResponse.body.error`). + /// public bool success; + + /// + /// Contains the raw error in short form if `success` is false. + /// This raw error might be interpreted by the client and is not shown in the UI. + /// Some predefined values exist. Values: + /// 'cancelled': the request was cancelled. + /// 'notStopped': the request may be retried once the adapter is in a 'stopped' state. etc. + /// public string message; - public int request_seq; + + /// + /// The command requested. + /// public string command; - public ResponseBody body; - public Response() { } + /// + /// Contains request result if success is true and error details if success is false. + /// + public object body; + + public Response() : base("response") { } public Response(Request req) : base("response") @@ -88,13 +180,13 @@ public Response(Request req) command = req.command; } - public void SetBody(ResponseBody bdy) + public void SetBody(object bdy) { success = true; body = bdy; } - public void SetErrorBody(string msg, ResponseBody bdy = null) + public void SetErrorBody(string msg, ErrorResponseBody bdy = null) { success = false; message = msg; @@ -102,266 +194,1003 @@ public void SetErrorBody(string msg, ResponseBody bdy = null) } } + ////////////////////////////////////////////////////////////////////////////// + /// EVENTS + ////////////////////////////////////////////////////////////////////////////// - public class Event : ProtocolMessage + /// + /// This event indicates that the debug adapter is ready to accept configuration requests (e.g. setBreakpoints, + /// setExceptionBreakpoints). + /// + public class InitializedEvent : Event { - [JsonProperty(PropertyName = "event")] - public string eventType { get; } + public InitializedEvent() + : base("initialized") { } + } - public dynamic body { get; } + public class StoppedEvent : Event + { + public StoppedEvent(int tid, string reasn, string txt = null) + : base("stopped", new + { + threadId = tid, + reason = reasn, + text = txt, + allThreadsStopped = true + }) + { } + } - public Event(string type, dynamic bdy = null) - : base("event") - { - eventType = type; - body = bdy; - } + public class ExitedEvent : Event + { + public ExitedEvent(int exCode) + : base("exited", new { exitCode = exCode }) { } + } + + public class TerminatedEvent : Event + { + public TerminatedEvent() + : base("terminated") { } + } + + public class ThreadEvent : Event + { + public ThreadEvent(string reasn, int tid) + : base("thread", new + { + reason = reasn, + threadId = tid + }) + { } + } + + public class OutputEvent : Event + { + public OutputEvent(string cat, string outpt) + : base("output", new + { + category = cat, + output = outpt + }) + { } } + ////////////////////////////////////////////////////////////////////////////// + /// REQUESTS and RESPONSES + ////////////////////////////////////////////////////////////////////////////// - /// Can be used to implement a debug adapter protocol - public abstract class ProtocolServer + public class InitializeRequestArguments { - protected const int BUFFER_SIZE = 4096; - protected static Regex CONTENT_LENGTH_MATCHER; + /// The ID of the client using this adapter. + public string clientID = null; + + /// The human-readable name of the client using this adapter. + public string clientName = null; + + /// The ID of the debug adapter. + public string adapterID; + + /// The ISO-639 locale of the client using this adapter, e.g. en-US or de-CH. + public string locale = null; + + /// If true all line numbers are 1-based (default). + public bool? linesStartAt1; + + /// If true all column numbers are 1-based (default). + public bool? columnsStartAt1; + + /// + /// Determines in what format paths are specified. The default is `path`, which is the native format. + /// Values: 'path', 'uri', etc. + /// + public string pathFormat = null; + + /// + /// Client supports the `type` attribute for variables. + /// + public bool? supportsVariableType; + + /** + * Client supports the paging of variables. + */ + public bool? supportsVariablePaging; + + /** + * Client supports the `runInTerminal` request. + */ + public bool? supportsRunInTerminalRequest; + + /** + * Client supports memory references. + */ + public bool? supportsMemoryReferences; + + /** + * Client supports progress reporting. + */ + public bool? supportsProgressReporting; + + /** + * Client supports the `invalidated` event. + */ + public bool? supportsInvalidatedEvent; + + /** + * Client supports the `memory` event. + */ + public bool? supportsMemoryEvent; + + /** + * Client supports the `argsCanBeInterpretedByShell` attribute on the + * `runInTerminal` request. + */ + public bool? supportsArgsCanBeInterpretedByShell; + + /** + * Client supports the `startDebugging` request. + */ + public bool? supportsStartDebuggingRequest; + + /** + * The client will interpret ANSI escape sequences in the display of + * `OutputEvent.output` and `Variable.value` fields when + * `Capabilities.supportsANSIStyling` is also enabled. + */ + public bool? supportsANSIStyling; + } + + + public class LaunchRequestArguments + { + /// Extension property (not part of DAP). + public string address; + + /// Extension property (not part of DAP). + public ushort? port; + + /// Extension property (not part of DAP). + public ExceptionOptions[] __exceptionOptions = null; + + /** + * If true, the launch request should launch the program without enabling + * debugging. + */ + public bool? noDebug; + + /** + * Arbitrary data from the previous, restarted session. + * The data is sent as the `restart` attribute of the `terminated` event. + * The client should leave the data intact. + */ + public object _restart = null; + } + + + public class DisconnectArguments + { + /** + * A value of true indicates that this `disconnect` request is part of a + * restart sequence. + */ + public bool? restart; + + /** + * Indicates whether the debuggee should be terminated when the debugger is + * disconnected. + * If unspecified, the debug adapter is free to do whatever it thinks is best. + * The attribute is only honored by a debug adapter if the corresponding + * capability `supportTerminateDebuggee` is true. + */ + public bool? terminateDebuggee; + + /** + * Indicates whether the debuggee should stay suspended when the debugger is + * disconnected. + * If unspecified, the debuggee should resume execution. + * The attribute is only honored by a debug adapter if the corresponding + * capability `supportSuspendDebuggee` is true. + */ + public bool? suspendDebuggee; + } + + public class NextArguments + { + /** + * Specifies the thread for which to resume execution for one step (of the + * given granularity). + */ + public int threadId; + + /** + * If this flag is true, all other suspended threads are not resumed. + */ + public bool? singleThread; + + /** + * Stepping granularity. If no granularity is specified, a granularity of + * `statement` is assumed. + */ + public SteppingGranularity? granularity; + } + + public class ContinueArguments + { + /** + * Specifies the active thread. If the debug adapter supports single thread + * execution (see `supportsSingleThreadExecutionRequests`) and the argument + * `singleThread` is true, only the thread with this ID is resumed. + */ + public int threadId; + + /** + * If this flag is true, execution is resumed only for the thread with given + * `threadId`. + */ + public bool? singleThread; + } + + + public class StepInArguments + { + /** + * Specifies the thread for which to resume execution for one step-into (of + * the given granularity). + */ + public int threadId; + + /** + * If this flag is true, all other suspended threads are not resumed. + */ + public bool? singleThread; + + /** + * Id of the target to step into. + */ + public int? targetId; + + /** + * Stepping granularity. If no granularity is specified, a granularity of + * `statement` is assumed. + */ + public SteppingGranularity? granularity; + } + + + public class StepOutArguments + { + /** + * Specifies the thread for which to resume execution for one step-out (of the + * given granularity). + */ + public int threadId; + + /** + * If this flag is true, all other suspended threads are not resumed. + */ + public bool? singleThread; + + /** + * Stepping granularity. If no granularity is specified, a granularity of + * `statement` is assumed. + */ + public SteppingGranularity? granularity; + } + + + public class SetExceptionBreakpointsArguments + { + /** + * Set of exception filters specified by their ID. The set of all possible + * exception filters is defined by the `exceptionBreakpointFilters` + * capability. The `filter` and `filterOptions` sets are additive. + */ + public string[] filters; + + /** + * Set of exception filters and their options. The set of all possible + * exception filters is defined by the `exceptionBreakpointFilters` + * capability. This attribute is only honored by a debug adapter if the + * corresponding capability `supportsExceptionFilterOptions` is true. The + * `filter` and `filterOptions` sets are additive. + */ + public ExceptionFilterOptions[] filterOptions = null; + + /** + * Configuration options for selected exceptions. + * The attribute is only honored by a debug adapter if the corresponding + * capability `supportsExceptionOptions` is true. + */ + public ExceptionOptions[] exceptionOptions = null; + } + + + public class SetBreakpointsArguments + { + /** + * The source location of the breakpoints; either `source.path` or + * `source.sourceReference` must be specified. + */ + public Source source; + + /** + * The code locations of the breakpoints. + */ + public SourceBreakpoint[] breakpoints = null; + + /** + * Deprecated: The code locations of the breakpoints. + */ + public int[] lines = null; + + /** + * A value of true indicates that the underlying source has been modified + * which results in new breakpoint locations. + */ + public bool? sourceModified; + } + + + public class StackTraceArguments + { + /** + * Retrieve the stacktrace for this thread. + */ + public int threadId; + + /** + * The index of the first frame to return; if omitted frames start at 0. + */ + public int? startFrame; + + /** + * The maximum number of frames to return. If levels is not specified or 0, + * all frames are returned. + */ + public int? levels; + + /** + * Specifies details on how to format the returned `StackFrame.name`. The + * debug adapter may format requested details in any way that would make sense + * to a developer. + * The attribute is only honored by a debug adapter if the corresponding + * capability `supportsValueFormattingOptions` is true. + */ + public StackFrameFormat format = null; + } + + + public class ScopesArguments + { + /** + * Retrieve the scopes for the stack frame identified by `frameId`. The + * `frameId` must have been obtained in the current suspended state. See + * 'Lifetime of Object References' in the Overview section for details. + */ + public int frameId; + } + + + public class VariablesArguments + { + /** + * The variable for which to retrieve its children. The `variablesReference` + * must have been obtained in the current suspended state. See 'Lifetime of + * Object References' in the Overview section for details. + */ + public int variablesReference; + + /** + * Filter to limit the child variables to either named or indexed. If omitted, + * both types are fetched. + * Values: 'indexed', 'named' + */ + public string filter = null; + + /** + * The index of the first variable to return; if omitted children start at 0. + * The attribute is only honored by a debug adapter if the corresponding + * capability `supportsVariablePaging` is true. + */ + public int? start; + + /** + * The number of variables to return. If count is missing or 0, all variables + * are returned. + * The attribute is only honored by a debug adapter if the corresponding + * capability `supportsVariablePaging` is true. + */ + public int? count; + + /** + * Specifies details on how to format the Variable values. + * The attribute is only honored by a debug adapter if the corresponding + * capability `supportsValueFormattingOptions` is true. + */ + public ValueFormat format = null; + } + + + public class EvaluateArguments + { + /** + * The expression to evaluate. + */ + public string expression; + + /** + * Evaluate the expression in the scope of this stack frame. If not specified, + * the expression is evaluated in the global scope. + */ + public int? frameId; + + /** + * The contextual line where the expression should be evaluated. In the + * 'hover' context, this should be set to the start of the expression being + * hovered. + */ + public int? line; + + /** + * The contextual column where the expression should be evaluated. This may be + * provided if `line` is also provided. + * + * It is measured in UTF-16 code units and the client capability + * `columnsStartAt1` determines whether it is 0- or 1-based. + */ + public int? column; + + /** + * The contextual source in which the `line` is found. This must be provided + * if `line` is provided. + */ + public Source source = null; + + /** + * The context in which the evaluate request is used. + * Values: + * 'watch': evaluate is called from a watch view context. + * 'repl': evaluate is called from a REPL context. + * 'hover': evaluate is called to generate the debug hover contents. + * This value should only be used if the corresponding capability + * `supportsEvaluateForHovers` is true. + * 'clipboard': evaluate is called to generate clipboard contents. + * This value should only be used if the corresponding capability + * `supportsClipboardContext` is true. + * 'variables': evaluate is called from a variables view context. + * etc. + */ + public string context = null; + + /** + * Specifies details on how to format the result. + * The attribute is only honored by a debug adapter if the corresponding + * capability `supportsValueFormattingOptions` is true. + */ + public ValueFormat format = null; + } + - protected static Encoding Encoding = Encoding.UTF8; - private int _sequenceNumber; - private readonly Dictionary> _pendingRequests; - private Stream _outputStream; - private readonly ByteBuffer _rawData; - private int _bodyLength; - private bool _stopRequested; - public ProtocolServer() + + + + + public class StackTraceResponseBody + { + public StackFrame[] stackFrames { get; } + public int totalFrames { get; } + + public StackTraceResponseBody(List frames, int total) { - CONTENT_LENGTH_MATCHER = new Regex(@"Content-Length: (\d+)\r\n\r\n"); - Encoding = Encoding.UTF8; - _sequenceNumber = 1; - _bodyLength = -1; - _rawData = new ByteBuffer(); - _pendingRequests = new Dictionary>(); + stackFrames = frames.ToArray(); + totalFrames = total; } + } - public async Task Start(Stream inputStream, Stream outputStream) + public class ScopesResponseBody + { + public Scope[] scopes { get; } + + public ScopesResponseBody(List scps) { - _outputStream = outputStream; + scopes = scps.ToArray(); + } + } - byte[] buffer = new byte[BUFFER_SIZE]; + public class VariablesResponseBody + { + public Variable[] variables { get; } - _stopRequested = false; - while (!_stopRequested) - { - var read = await inputStream.ReadAsync(buffer, 0, buffer.Length); + public VariablesResponseBody(List vars) + { + variables = vars.ToArray(); + } + } - if (read == 0) - { - // end of stream - break; - } + public class ThreadsResponseBody + { + public Thread[] threads { get; } - if (read > 0) - { - _rawData.Append(buffer, read); - ProcessData(); - } - } + public ThreadsResponseBody(List ths) + { + threads = ths.ToArray(); } + } + + public class EvaluateResponseBody + { + public string result { get; } + public int variablesReference { get; } - public void Stop() + public EvaluateResponseBody(string value, int reff = 0) { - _stopRequested = true; + result = value; + variablesReference = reff; } + } - public void SendEvent(Event e) + public class SetBreakpointsResponseBody + { + public Breakpoint[] breakpoints { get; } + + public SetBreakpointsResponseBody(List bpts = null) { - SendMessage(e); + if (bpts == null) + breakpoints = new Breakpoint[0]; + else + breakpoints = bpts.ToArray(); } + } - public Task SendRequest(string command, dynamic args) - { - var tcs = new TaskCompletionSource(); + public class SetVariablesResponseBody + { + public string value { get; } + public string type { get; } + public int variablesReference { get; } - Request request = null; - lock (_pendingRequests) - { - request = new Request(_sequenceNumber++, command, args); + public SetVariablesResponseBody(string value, string type, int variablesReference) + { + this.value = value; + this.type = type; + this.variablesReference = variablesReference; + } + } - // wait for response - _pendingRequests.Add(request.seq, tcs); - } + public class ContinueResponseBody + { + public bool allThreadsContinued = true; + } - SendMessage(request); + public class SetFunctionBreakpointsBody + { + public Breakpoint[] breakpoints { get; } - return tcs.Task; + public SetFunctionBreakpointsBody(Breakpoint[] breakpoints) + { + this.breakpoints = breakpoints; } + } + + ////////////////////////////////////////////////////////////////////////////// + /// REVERSE REQUESTS + ////////////////////////////////////////////////////////////////////////////// - protected abstract void DispatchRequest(string command, dynamic args, Response response); + ////////////////////////////////////////////////////////////////////////////// + /// TYPES + ////////////////////////////////////////////////////////////////////////////// - private void ProcessData() + /// + /// Information about a breakpoint created in setBreakpoints, setFunctionBreakpoints, setInstructionBreakpoints, or + /// setDataBreakpoints requests. + /// + public class Breakpoint + { + public int id { get; } + public bool verified { get; } + public string message { get; } + public Source source { get; } + public int line { get; } + public int column { get; } + public int endLine { get; } + public int endColumn { get; } + + public Breakpoint(bool verified, int line, int column, string logMessage) { - // assume that we don't get fragmented messages - while (true) - { - if (_bodyLength >= 0) - { - if (_rawData.Length >= _bodyLength) - { - var buf = _rawData.RemoveFirst(_bodyLength); - _bodyLength = -1; - string data = Encoding.GetString(buf); - Logger.LogTrace($"received data: {data}"); - Dispatch(data); - continue; // there may be more complete messages to process - } - } - else - { - string s = _rawData.GetString(Encoding); - if (string.IsNullOrWhiteSpace(s)) - { - _rawData.RemoveFirst(s.Length); - break; - } - Match m = CONTENT_LENGTH_MATCHER.Match(s); - if (m.Success && m.Groups.Count == 2) - { - _bodyLength = Convert.ToInt32(m.Groups[1].ToString()); - _rawData.RemoveFirst(m.Index + "Content-Length: ".Length + m.Groups[1].Length + 4); - continue; // try to handle a complete message - } - else - { - Logger.LogWarn(@"could not regex 'Content-Length: (\d+)' in: " + s); - } - } - - break; - } + this.verified = verified; + this.line = line; + this.column = column; + this.message = logMessage; } + } + + /// + /// Information about the capabilities of a debug adapter. + /// + public class Capabilities + { + /// + /// The debug adapter supports the `configurationDone` request. + /// + public bool supportsConfigurationDoneRequest = false; + + /// + /// The debug adapter supports function breakpoints. + /// + public bool supportsFunctionBreakpoints = false; + + /// + /// The debug adapter supports conditional breakpoints. + /// + public bool supportsConditionalBreakpoints = false; + + /// + /// The debug adapter supports breakpoints that break execution after a + /// specified number of hits. + /// + public bool supportsHitConditionalBreakpoints = false; + + /// + /// The debug adapter supports a (side effect free) `evaluate` request for data hovers. + /// + public bool supportsEvaluateForHovers = false; + + /// + /// Available exception filter options for the `setExceptionBreakpoints` request. + /// + public ExceptionBreakpointsFilter[] exceptionBreakpointFilters = new ExceptionBreakpointsFilter[0]; + + /// + /// The debug adapter supports setting a variable to a value. + /// + public bool supportsSetVariable = false; - private void Dispatch(string req) + /// + /// The debug adapter supports `exceptionOptions` on the `setExceptionBreakpoints` request. + /// + public bool supportsExceptionOptions = false; + + /// + /// The debug adapter supports log points by interpreting the `logMessage` attribute of the `SourceBreakpoint`. + /// + public bool supportsLogPoints = false; + } + + + /// + /// An ExceptionBreakpointsFilter is shown in the UI as an filter option for configuring how exceptions are dealt with. + /// + public class ExceptionBreakpointsFilter + { + public string filter { get; } + public string label { get; } + + [JsonProperty("default")] + public bool? defaultValue { get; } + + public ExceptionBreakpointsFilter(string filter, string label, bool defaultValue = false) { - var message = JsonConvert.DeserializeObject(req); - if (message != null) - { - switch (message.type) - { - case "request": - { - var request = JsonConvert.DeserializeObject(req); - var response = new Response(request); - DispatchRequest(request.command, request.arguments, response); - SendMessage(response); - } - break; - - case "response": - { - var response = JsonConvert.DeserializeObject(req); - int seq = response.request_seq; - lock (_pendingRequests) - { - if (_pendingRequests.ContainsKey(seq)) - { - var tcs = _pendingRequests[seq]; - _pendingRequests.Remove(seq); - tcs.SetResult(response); - } - } - } - break; - default: - Logger.LogWarn($"unsupported message type: {message.type}"); - break; - } - } - else - { - Logger.LogError($"could not deserialize provided request into a ProtocolMessage: {req}"); - } + this.filter = filter; + this.label = label; + this.defaultValue = defaultValue; } + } - protected void SendMessage(ProtocolMessage message) - { - if (message.seq == 0) - { - message.seq = _sequenceNumber++; - } + public class ExceptionOptions + { + /** + * A path that selects a single or multiple exceptions in a tree. If `path` is + * missing, the whole tree is selected. + * By convention the first segment of the path is a category that is used to + * group exceptions in the UI. + */ + public ExceptionPathSegment[] path; - var data = ConvertToBytes(message); - try - { - _outputStream.Write(data, 0, data.Length); - _outputStream.Flush(); - } - catch (Exception e) - { - Logger.LogError($"{e.Message} {e.StackTrace}"); - } - } + /** + * Condition when a thrown exception should result in a break. + */ + // TODO: add ExceptionBreakMode + public string breakMode; + } - private static byte[] ConvertToBytes(ProtocolMessage request) - { - var asJson = JsonConvert.SerializeObject(request); - Logger.LogTrace($"sent data: {asJson}"); - byte[] jsonBytes = Encoding.GetBytes(asJson); - string header = string.Format($"Content-Length: {jsonBytes.Length}\r\n\r\n"); - byte[] headerBytes = Encoding.GetBytes(header); + public class ExceptionFilterOptions + { + /** + * ID of an exception filter returned by the `exceptionBreakpointFilters` + * capability. + */ + public string filterId; - byte[] data = new byte[headerBytes.Length + jsonBytes.Length]; - Buffer.BlockCopy(headerBytes, 0, data, 0, headerBytes.Length); - Buffer.BlockCopy(jsonBytes, 0, data, headerBytes.Length, jsonBytes.Length); + /** + * An expression for conditional exceptions. + * The exception breaks into the debugger if the result of the condition is + * true. + */ + public string condition = null; + + /** + * The mode of this exception breakpoint. If defined, this must be one of the + * `breakpointModes` the debug adapter advertised in its `Capabilities`. + */ + public string mode = null; + } + + + public class ExceptionPathSegment + { + /** + * If false or missing this segment matches the names provided, otherwise it + * matches anything except the names provided. + */ + public bool? negate; - return data; + /** + * Depending on the value of `negate` the names that should match or not + * match. + */ + public string[] names; + } + + + /// + /// A structured message object. Used to return errors from requests. + /// + public class Message + { + /// + /// Unique (within a debug adapter implementation) identifier for the message. The purpose of these error IDs is to help extension authors that have the requirement that every user visible error message needs a corresponding error number, so that users or customer support can find information about the specific error more easily. + /// + public int id; + + /// + /// A format string for the message. Embedded variables have the form `{name}`. If variable name starts with an underscore character, the variable does not contain user data (PII) and can be safely used for telemetry purposes. + public string format; + + /// + /// An object used as a dictionary for looking up the variables in the format string. + /// + public Dictionary variables = null; + + public bool? showUser; + + public bool? sendTelemetry; + + public Message(int id, string format, Dictionary variables = null, bool user = true, bool telemetry = false) + { + this.id = id; + this.format = format; + this.variables = variables; + this.showUser = user; + this.sendTelemetry = telemetry; } } - /// encapsulates a byte array (akin to a bytebuffer in Python) - class ByteBuffer + /// + /// A Scope is a named container for variables. Optionally a scope can map to a source or a range within a source. + /// + public class Scope { - private byte[] _buffer; + public string name { get; } + public int variablesReference { get; } + public bool expensive { get; } - public ByteBuffer() + public Scope(string name, int variablesReference, bool expensive = false) { - _buffer = new byte[0]; + this.name = name; + this.variablesReference = variablesReference; + this.expensive = expensive; } + } + + /// + /// A Source is a descriptor for source code. It is returned from the debug adapter as part of a StackFrame and it is + /// used by clients when specifying breakpoints. + /// + public class Source + { + public string name { get; } + public string path { get; } + public int sourceReference { get; } + public string presentationHint { get; } - public int Length + public Source(string name, string path, int sourceReference, string hint) { - get { return _buffer.Length; } + this.name = name; + this.path = path; + this.sourceReference = sourceReference; + this.presentationHint = hint; } + } + + /// + /// Properties of a breakpoint or logpoint passed to the setBreakpoints request. + /// + public class SourceBreakpoint + { + public int line; + public int column; + public string condition; + public string hitCondition; + public string logMessage; + } + + + /// + /// A Stackframe contains the source location. + /// + public class StackFrame + { + public int id { get; } + public Source source { get; } + public int line { get; } + public int column { get; } + public string name { get; } + public string presentationHint { get; } - public string GetString(Encoding enc) + public StackFrame(int id, string name, Source source, int line, int column, string hint) { - return enc.GetString(_buffer); + this.id = id; + this.name = name; + this.source = source; + + // These should NEVER be negative + this.line = Math.Max(0, line); + this.column = Math.Max(0, column); + + this.presentationHint = hint; } + } + - public void Append(byte[] b, int length) + public class StackFrameFormat : ValueFormat + { + /** + * Displays parameters for the stack frame. + */ + public bool? parameters; + + /** + * Displays the types of parameters for the stack frame. + */ + public bool? parameterTypes; + + /** + * Displays the names of parameters for the stack frame. + */ + public bool? parameterNames; + + /** + * Displays the values of parameters for the stack frame. + */ + public bool? parameterValues; + + /** + * Displays the line number of the stack frame. + */ + public bool? line; + + /** + * Displays the module of the stack frame. + */ + public bool? module; + + /** + * Includes all stack frames, including those the debug adapter might + * otherwise hide. + */ + public bool? includeAll; + } + + + + + public class StepInTarget + { + /** + * Unique identifier for a step-in target. + */ + public int id; + + /** + * The name of the step-in target (shown in the UI). + */ + public string label; + + /** + * The line of the step-in target. + */ + public int? line; + + /** + * Start position of the range covered by the step in target. It is measured + * in UTF-16 code units and the client capability `columnsStartAt1` determines + * whether it is 0- or 1-based. + */ + public int? column; + + /** + * The end line of the range covered by the step-in target. + */ + public int? endLine; + + /** + * End position of the range covered by the step in target. It is measured in + * UTF-16 code units and the client capability `columnsStartAt1` determines + * whether it is 0- or 1-based. + */ + public int? endColumn; + } + + + public enum SteppingGranularity + { + statement, + line, + instruction + } + + + /// + /// A Thread. + /// + public class Thread + { + public int id { get; } + public string name { get; } + + public Thread(int id, string name) { - byte[] newBuffer = new byte[_buffer.Length + length]; - Buffer.BlockCopy(_buffer, 0, newBuffer, 0, _buffer.Length); - Buffer.BlockCopy(b, 0, newBuffer, _buffer.Length, length); - _buffer = newBuffer; + this.id = id; + if (name == null || name.Length == 0) + { + this.name = string.Format("Thread #{0}", id); + } + else + { + this.name = name; + } } + } + + + /// + /// A Variable is a name/value pair. + /// The type attribute is shown if space permits or when hovering over the variable’s name. + /// The kind attribute is used to render additional properties of the variable, e.g. different icons can be used to + /// indicate that a variable is public or private. + /// If the value is structured (has children), a handle is provided to retrieve the children with the variables request. If the number of named or indexed children is large, the numbers should be returned via the namedVariables and indexedVariables attributes. The client can use this information to present the children in a paged UI and fetch them in chunks. + /// + public class Variable + { + public string name; + public string value; + public string type = null; + public int variablesReference = 0; - public byte[] RemoveFirst(int n) + public Variable(string name, string value, string type, int variablesReference = 0) { - byte[] b = new byte[n]; - Buffer.BlockCopy(_buffer, 0, b, 0, n); - byte[] newBuffer = new byte[_buffer.Length - n]; - Buffer.BlockCopy(_buffer, n, newBuffer, 0, _buffer.Length - n); - _buffer = newBuffer; - return b; + this.name = name; + this.value = value; + this.type = type; + this.variablesReference = variablesReference; } } + + + public class ValueFormat + { + /** + * Display the value in hex. + */ + public bool? hex; + } } + diff --git a/unity-debug-adapter/ProtocolServer.cs b/unity-debug-adapter/ProtocolServer.cs new file mode 100644 index 0000000..d2e2e2f --- /dev/null +++ b/unity-debug-adapter/ProtocolServer.cs @@ -0,0 +1,261 @@ +using System; +using System.Text; +using System.IO; +using System.Threading.Tasks; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace UnityDebugAdapter +{ + /// + /// Can be used to implement a debug adapter protocol + /// + public abstract class ProtocolServer + { + protected static readonly int BUFFER_SIZE = 4096; + protected static Regex CONTENT_LENGTH_MATCHER = new Regex(@"Content-Length: (\d+)\r\n\r\n"); + + private int _sequenceNumber = 1; + // TODO: use concurrent Dictionary instead... + private readonly Dictionary> _pendingRequests + = new Dictionary>(); + + private Stream _outputStream; + + private readonly ByteBuffer _rawData = new ByteBuffer(); + private int _bodyLength = -1; + + private bool _stopRequested; + + public async Task Start(Stream inputStream, Stream outputStream) + { + _outputStream = outputStream; + + byte[] buffer = new byte[BUFFER_SIZE]; + + _stopRequested = false; + while (!_stopRequested) + { + var read = await inputStream.ReadAsync(buffer, 0, buffer.Length); + + if (read == 0) + { + // end of stream + Logger.LogTrace("end of stream reached - exiting the debug adapter"); + break; + } + + if (read > 0) + { + _rawData.Append(buffer, read); + ProcessData(); + } + } + } + + + public void Stop() + { + _stopRequested = true; + } + + + public void SendEvent(Event e) + { + SendMessage(e); + } + + + public Task SendRequest(string command, object args) + { + var tcs = new TaskCompletionSource(); + + Request request = null; + lock (_pendingRequests) + { + request = new Request(_sequenceNumber++, command, args); + + // wait for response + _pendingRequests.Add(request.seq, tcs); + } + + SendMessage(request); + + return tcs.Task; + } + + + protected abstract void DispatchRequest(int reqSeq, string command, JToken args); + + + private void ProcessData() + { + while (true) + { + if (_bodyLength >= 0) + { + if (_rawData.Length >= _bodyLength) + { + var buf = _rawData.RemoveFirst(_bodyLength); + string data = Encoding.UTF8.GetString(buf); + Logger.LogTrace("received data: Content-Length: ({0})rnrn{{{1}}}", _bodyLength, data); + Dispatch(data); + _bodyLength = -1; + continue; // there may be more complete messages to process + } + else // currently held raw data is insufficient to completely re-construct the body + { + break; + } + } + else // (_bodyLength == -1) means we got a new message (i.e., a message with Content-Length: (\d+): \r\n\r\n{body}) + { + string s = _rawData.GetString(); + + if (string.IsNullOrWhiteSpace(s)) + { + _rawData.RemoveFirst(s.Length); + break; + } + + Match m = CONTENT_LENGTH_MATCHER.Match(s); + if (m.Success && m.Groups.Count == 2) + { + _bodyLength = Convert.ToInt32(m.Groups[1].ToString()); + _rawData.RemoveFirst(m.Index + "Content-Length: ".Length + m.Groups[1].Length + 4); + continue; // try to handle a complete message + } + else + { + // TODO: do proper exit strategy here + Logger.LogWarn(@"could not regex 'Content-Length: (\d+)' in: {0}", s); + } + } + + break; + } + } + + private void Dispatch(string req) + { + var message = JsonConvert.DeserializeObject(req); + if (message == null) + { + Logger.LogError("could not deserialize provided request into a ProtocolMessage: {0}", req); + return; + } + switch (message.type) + { + case "request": + { + var request = JObject.Parse(req); + var reqSeq = (int)request["seq"]; + var cmd = (string)request["command"]; + var args = request["arguments"]; + DispatchRequest(reqSeq, cmd, args); + } + break; + + case "response": + { + var response = JsonConvert.DeserializeObject(req); + int seq = response.request_seq; + lock (_pendingRequests) + { + if (_pendingRequests.ContainsKey(seq)) + { + var tcs = _pendingRequests[seq]; + _pendingRequests.Remove(seq); + tcs.SetResult(response); + } + } + } + break; + case "event": + // we don't care about events for the moment + break; + default: + Logger.LogWarn("unsupported message type: {0}", message.type); + break; + } + } + + + protected void SendMessage(ProtocolMessage message) + { + if (message.seq == 0) + message.seq = _sequenceNumber++; + + var data = ConvertToBytes(message); + try + { + _outputStream.Write(data, 0, data.Length); + _outputStream.Flush(); + } + catch (Exception e) + { + Logger.LogError("{0} {1}", e.Message, e.StackTrace); + } + + Logger.LogTrace("sent {0}: {1}", message.type, message); + } + + private static byte[] ConvertToBytes(ProtocolMessage request) + { + var asJson = JsonConvert.SerializeObject(request); + byte[] jsonBytes = Encoding.UTF8.GetBytes(asJson); + + string header = string.Format($"Content-Length: {jsonBytes.Length}\r\n\r\n"); + byte[] headerBytes = Encoding.UTF8.GetBytes(header); + + byte[] data = new byte[headerBytes.Length + jsonBytes.Length]; + Buffer.BlockCopy(headerBytes, 0, data, 0, headerBytes.Length); + Buffer.BlockCopy(jsonBytes, 0, data, headerBytes.Length, jsonBytes.Length); + + return data; + } + } + + + /// Encapsulates a byte array (akin to a bytebuffer in Python). + class ByteBuffer + { + private byte[] _buffer = Array.Empty(); + + public int Length => _buffer.Length; + + public string GetString() => Encoding.UTF8.GetString(_buffer); + + + /// + /// Pops a string from internal array [0, [. + /// + /// + /// + public string PopString(int _) + { + return string.Empty; + } + + // TODO: replace this fuckin garbage of a mess + public void Append(byte[] b, int length) + { + byte[] newBuffer = new byte[_buffer.Length + length]; + Buffer.BlockCopy(_buffer, 0, newBuffer, 0, _buffer.Length); + Buffer.BlockCopy(b, 0, newBuffer, _buffer.Length, length); + _buffer = newBuffer; + } + + public byte[] RemoveFirst(int n) + { + byte[] b = new byte[n]; + Buffer.BlockCopy(_buffer, 0, b, 0, n); + byte[] newBuffer = new byte[_buffer.Length - n]; + Buffer.BlockCopy(_buffer, n, newBuffer, 0, _buffer.Length - n); + _buffer = newBuffer; + return b; + } + } +} diff --git a/unity-debug-adapter/UnityDebugSession.cs b/unity-debug-adapter/UnityDebugSession.cs index fe7482b..d9bdcb4 100644 --- a/unity-debug-adapter/UnityDebugSession.cs +++ b/unity-debug-adapter/UnityDebugSession.cs @@ -1,5 +1,3 @@ -#pragma warning disable IDE1006, IDE0003, IDE0038, IDE0001, IDE0031 - using System; using System.Collections.Generic; using System.IO; @@ -8,7 +6,6 @@ using System.Threading; using Mono.Debugging.Client; using Mono.Debugging.Soft; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace UnityDebugAdapter @@ -19,7 +16,7 @@ public class UnityDebuggerSession : SoftDebuggerSession { protected override void OnExit() { - this.Detach(); + Detach(); } } @@ -74,10 +71,17 @@ public UnityDebugSession() return true; }; - m_Session.LogWriter = (isStdErr, text) => - { - SendOutput(isStdErr ? "stderr" : "stdout", text); - }; + // these are commented because they absolutely flood the REPL for front-end DAP clients + // m_Session.LogWriter = (isStdErr, text) => + // { + // SendOutput(isStdErr ? "stderr" : "stdout", text); + // }; + // + // + // m_Session.OutputWriter = (isStdErr, text) => + // { + // SendOutput(isStdErr ? "stderr" : "stdout", text); + // }; m_Session.TargetStopped += (sender, e) => { @@ -186,28 +190,46 @@ public UnityDebugSession() SendEvent(new ThreadEvent("exited", tid)); }; - m_Session.OutputWriter = (isStdErr, text) => - { - SendOutput(isStdErr ? "stderr" : "stdout", text); - }; - Logger.LogInfo("done constructing UnityDebugSession"); } public Mono.Debugging.Client.StackFrame Frame { get; set; } - public override void Initialize(Response response, dynamic args) + public override void Initialize(int reqSeq, JToken args) { + + var initilizeReqArgs = args.ToObject(); + _clientLinesStartAt1 = initilizeReqArgs.linesStartAt1 ?? true; + + var pathFormat = initilizeReqArgs.pathFormat; + if (pathFormat != null) + { + switch (pathFormat) + { + case "uri": + _clientPathsAreURI = true; + break; + case "path": + _clientPathsAreURI = false; + break; + default: + SendErrorResponse(reqSeq, "initialize", 1015, "initialize: bad value '{_format}' for pathFormat", + new Dictionary { { "_format", pathFormat } }); + return; + } + } + var os = Environment.OSVersion; if (os.Platform != PlatformID.MacOSX && os.Platform != PlatformID.Unix && os.Platform != PlatformID.Win32NT) { - SendErrorResponse(response, 3000, "Mono Debug is not supported on this platform ({_platform}).", new { _platform = os.Platform.ToString() }, true, true); + SendErrorResponse(reqSeq, "initialize", 3000, "Mono Debug is not supported on this platform ({_platform}).", + new Dictionary { { "_platform", os.Platform.ToString() } }, true, true); return; } SendOutput("stdout", "UnityDebug: Initializing"); - SendResponse(response, new Capabilities() + var capabilities = new Capabilities() { // This debug adapter does not need the configurationDoneRequest. supportsConfigurationDoneRequest = false, @@ -229,71 +251,88 @@ public override void Initialize(Response response, dynamic args) // This debug adapter does not support exception breakpoint filters exceptionBreakpointFilters = new ExceptionBreakpointsFilter[0] - }); + }; + var response = new Response() + { + command = "initialize", + request_seq = reqSeq, + success = true, + body = capabilities, + }; + SendMessage(response); // Mono Debug is ready to accept breakpoints immediately SendEvent(new InitializedEvent()); } - public override void Launch(Response response, dynamic args) + public override void Launch(int reqSeq, JToken args) + { + AttachInternal(args); + var response = new Response() + { + command = "launch", + request_seq = reqSeq, + success = true, + }; + SendMessage(response); + } + + public override void Attach(int reqSeq, JToken args) { - Attach(response, args); + AttachInternal(args); + var response = new Response() + { + command = "attach", + request_seq = reqSeq, + success = true, + }; + SendMessage(response); } - public override void Attach(Response response, dynamic args) + void AttachInternal(JToken args) { - string address_str = GetString(args, "address"); - if (address_str == null) + var attachArgs = args.ToObject(); + + if (attachArgs.address == null) { Logger.LogError("expected \"address\" property string in attach's arguments request"); return; } - IPAddress address = IPAddress.Parse(address_str); - int port = GetInt(args, "port", -1); - if (port == -1) + IPAddress address = IPAddress.Parse(attachArgs.address); + ushort port; + if (attachArgs.port == null) { Logger.LogError("expected \"port\" property int with a valid port in attach's arguments request"); return; } + port = attachArgs.port.Value; - SetExceptionBreakpoints(args.__exceptionOptions); + SetExceptionBreakpoints(attachArgs.__exceptionOptions); Connect(address, port); SendOutput("stdout", $"UnityDebugAdapter: attached to Unity Mono runtime endpoint via {address}:{port}"); - - SendResponse(response); } - static string CleanPath(string pathToEditorInstanceJson) - { - var osVersion = Environment.OSVersion; - if (osVersion.Platform == PlatformID.MacOSX || osVersion.Platform == PlatformID.Unix) - { - return pathToEditorInstanceJson; - } - - return pathToEditorInstanceJson.TrimStart('/'); - } void Connect(IPAddress address, int port) { Logger.LogInfo($"connecting to: {address}:{port}"); lock (m_Lock) { - var args0 = new SoftDebuggerConnectArgs(string.Empty, address, port) + var startArgs = new SoftDebuggerConnectArgs(string.Empty, address, port) { MaxConnectionAttempts = MAX_CONNECTION_ATTEMPTS, TimeBetweenConnectionAttempts = CONNECTION_ATTEMPT_INTERVAL }; - m_Session.Run(new SoftDebuggerStartInfo(args0), m_DebuggerSessionOptions); + m_Session.Run(new SoftDebuggerStartInfo(startArgs), m_DebuggerSessionOptions); m_DebuggeeExecuting = true; } } - void SetExceptionBreakpoints(dynamic exceptionOptions) + void SetExceptionBreakpoints(ExceptionOptions[] exceptionOptions) { if (exceptionOptions == null) { @@ -308,36 +347,21 @@ void SetExceptionBreakpoints(dynamic exceptionOptions) m_Catchpoints.Clear(); - var exceptions = exceptionOptions.ToObject(); - for (var i = 0; i < exceptions.Length; i++) + foreach (ExceptionOptions exception in exceptionOptions) { - var exception = exceptions[i]; - string exName = null; string exBreakMode = exception.breakMode; - if (exception.path != null) - { - var paths = exception.path.ToObject(); - var path = paths[0]; - if (path.names != null) - { - var names = path.names.ToObject(); - if (names.Length > 0) - { - exName = names[0]; - } - } - } + var path = exception.path?[0]; + if (path.names != null && path.names.Length > 0) + exName = path.names[0]; if (exName != null && exBreakMode == "always") - { m_Catchpoints.Add(m_Session.Breakpoints.AddCatchpoint(exName)); - } } } - public override void Disconnect(Response response, dynamic args) + public override void Disconnect(int reqSeq, JToken args) { lock (m_Lock) { @@ -354,19 +378,43 @@ public override void Disconnect(Response response, dynamic args) } SendOutput("stdout", "UnityDebugAdapter: Disconnected"); - SendResponse(response); + var response = new Response() + { + command = "disconnect", + request_seq = reqSeq, + success = true, + }; + SendMessage(response); + + // as per the specification, the debug adapter should terminate itself + DebuggerKill(); + Environment.Exit(0); } - public override void SetFunctionBreakpoints(Response response, dynamic arguments) + public override void SetFunctionBreakpoints(int reqSeq, JToken args) { - var breakpoints = new List(); - SendResponse(response, new SetFunctionBreakpointsBody(breakpoints.ToArray())); + var breakpoints = new List(); + var response = new Response() + { + command = "setFunctionBreakpoints", + request_seq = reqSeq, + success = true, + body = new SetFunctionBreakpointsBody(breakpoints.ToArray()) + }; + SendMessage(response); } - public override void Continue(Response response, dynamic arguments) + public override void Continue(int reqSeq, JToken args) { WaitForSuspend(); - SendResponse(response, new ContinueResponseBody()); + var response = new Response() + { + command = "continue", + request_seq = reqSeq, + success = true, + body = new ContinueResponseBody() + }; + SendMessage(response); lock (m_Lock) { if (m_Session == null || m_Session.IsRunning || m_Session.HasExited) return; @@ -376,10 +424,16 @@ public override void Continue(Response response, dynamic arguments) } } - public override void Next(Response response, dynamic arguments) + public override void Next(int reqSeq, JToken args) { WaitForSuspend(); - SendResponse(response); + var response = new Response() + { + command = "next", + request_seq = reqSeq, + success = true, + }; + SendMessage(response); lock (m_Lock) { if (m_Session == null || m_Session.IsRunning || m_Session.HasExited) return; @@ -389,10 +443,16 @@ public override void Next(Response response, dynamic arguments) } } - public override void StepIn(Response response, dynamic arguments) + public override void StepIn(int reqSeq, JToken args) { WaitForSuspend(); - SendResponse(response); + var response = new Response() + { + command = "stepIn", + request_seq = reqSeq, + success = true, + }; + SendMessage(response); lock (m_Lock) { if (m_Session == null || m_Session.IsRunning || m_Session.HasExited) return; @@ -402,10 +462,16 @@ public override void StepIn(Response response, dynamic arguments) } } - public override void StepOut(Response response, dynamic arguments) + public override void StepOut(int reqSeq, JToken args) { WaitForSuspend(); - SendResponse(response); + var response = new Response() + { + command = "stepOut", + request_seq = reqSeq, + success = true, + }; + SendMessage(response); lock (m_Lock) { if (m_Session == null || m_Session.IsRunning || m_Session.HasExited) return; @@ -415,9 +481,15 @@ public override void StepOut(Response response, dynamic arguments) } } - public override void Pause(Response response, dynamic arguments) + public override void Pause(int reqSeq, JToken args) { - SendResponse(response); + var response = new Response() + { + command = "pause", + request_seq = reqSeq, + success = true, + }; + SendMessage(response); PauseDebugger(); } @@ -430,16 +502,17 @@ void PauseDebugger() } } - protected override void SetVariable(Response response, object arguments) + protected override void SetVariable(int reqSeq, JToken args) { - var reference = GetInt(arguments, "variablesReference", -1); + var reference = (int)args["variablesReference"]; if (reference == -1) { - SendErrorResponse(response, 3009, "variables: property 'variablesReference' is missing", null, false, true); + SendErrorResponse(reqSeq, "setVariable", 3009, "variables: property 'variablesReference' is missing", + null, false, true); return; } - var value = GetString(arguments, "value"); + var value = (string)args["value"]; if (m_VariableHandles.TryGet(reference, out var children)) { if (children != null && children.Length > 0) @@ -455,50 +528,71 @@ protected override void SetVariable(Response response, object arguments) continue; v.WaitHandle.WaitOne(); var variable = CreateVariable(v); - if (variable.name == GetString(arguments, "name")) + if (variable.name == (string)args["name"]) { v.Value = value; - SendResponse(response, new SetVariablesResponseBody(value, variable.type, variable.variablesReference)); + var response = new Response() + { + command = "setVariable", + request_seq = reqSeq, + success = true, + body = new SetVariablesResponseBody(value, variable.type, variable.variablesReference), + }; + SendMessage(response); } } } } } - public override void SetExceptionBreakpoints(Response response, dynamic arguments) + public override void SetExceptionBreakpoints(int reqSeq, JToken args) { - SetExceptionBreakpoints(arguments.exceptionOptions); - SendResponse(response); + var _args = args.ToObject(); + SetExceptionBreakpoints(_args.exceptionOptions); + var response = new Response() + { + command = "setExceptionBreakpoints", + request_seq = reqSeq, + success = true, + }; + SendMessage(response); } - public override void SetBreakpoints(Response response, dynamic arguments) + public override void SetBreakpoints(int reqSeq, JToken args) { string path = null; + var _args = args.ToObject(); + var response = new Response() + { + command = "setBreakpoints", + request_seq = reqSeq, + success = true, + }; - if (arguments.source != null) + if (_args.source != null) { - var p = (string)arguments.source.path; + var p = _args.source.path; if (p != null && p.Trim().Length > 0) - { path = p; - } } if (path == null) { - SendErrorResponse(response, 3010, "setBreakpoints: property 'source' is empty or misformed", null, false, true); + SendErrorResponse(reqSeq, "setBreakpoints", 3010, "setBreakpoints: property 'source' is empty or misformed", + null, false, true); return; } if (!HasMonoExtension(path)) { // we only support breakpoints in files mono can handle - SendResponse(response, new SetBreakpointsResponseBody()); + response.body = new SetBreakpointsResponseBody(); + SendMessage(response); return; } - SourceBreakpoint[] newBreakpoints = getBreakpoints(arguments, "breakpoints"); - bool sourceModified = (bool)arguments.sourceModified; + SourceBreakpoint[] newBreakpoints = _args.breakpoints ?? Array.Empty(); + bool sourceModified = _args.sourceModified ?? false; var lines = newBreakpoints.Select(bp => bp.line); Dictionary dictionary = null; @@ -523,7 +617,7 @@ public override void SetBreakpoints(Response response, dynamic arguments) m_Breakpoints[path] = dictionary; } - var responseBreakpoints = new List(); + var responseBreakpoints = new List(); foreach (var breakpoint in newBreakpoints) { if (!dictionary.ContainsKey(breakpoint.line)) @@ -538,30 +632,33 @@ public override void SetBreakpoints(Response response, dynamic arguments) bp.TraceExpression = breakpoint.logMessage; } dictionary[breakpoint.line] = bp; - responseBreakpoints.Add(new UnityDebugAdapter.Breakpoint(true, breakpoint.line, breakpoint.column, breakpoint.logMessage)); + responseBreakpoints.Add(new Breakpoint(true, breakpoint.line, breakpoint.column, breakpoint.logMessage)); } catch (Exception e) { Logger.LogError($"SetBreakpoints error: msg: {e.Message}, stacktrace: {e.StackTrace}"); - SendErrorResponse(response, 3011, "setBreakpoints: " + e.Message, null, false, true); - responseBreakpoints.Add(new UnityDebugAdapter.Breakpoint(false, breakpoint.line, breakpoint.column, e.Message)); + SendErrorResponse(reqSeq, "setBreakpoints", 3011, "setBreakpoints: " + e.Message, + null, false, true); + responseBreakpoints.Add(new Breakpoint(false, breakpoint.line, breakpoint.column, e.Message)); } } else { dictionary[breakpoint.line].ConditionExpression = breakpoint.condition; - responseBreakpoints.Add(new UnityDebugAdapter.Breakpoint(true, breakpoint.line, breakpoint.column, breakpoint.logMessage)); + responseBreakpoints.Add(new Breakpoint(true, breakpoint.line, breakpoint.column, breakpoint.logMessage)); } } - SendResponse(response, new SetBreakpointsResponseBody(responseBreakpoints)); + response.body = new SetBreakpointsResponseBody(responseBreakpoints); + SendMessage(response); } - public override void StackTrace(Response response, dynamic arguments) + public override void StackTrace(int reqSeq, JToken args) { - int maxLevels = GetInt(arguments, "levels", 10); - int startFrame = GetInt(arguments, "startFrame", 0); - int threadReference = GetInt(arguments, "threadId", 0); + var _args = args.ToObject(); + int maxLevels = _args.levels ?? 10; + int startFrame = _args.startFrame ?? 0; + int threadReference = _args.threadId; WaitForSuspend(); @@ -570,13 +667,10 @@ public override void StackTrace(Response response, dynamic arguments) { // Console.Error.WriteLine("stackTrace: unexpected: active thread should be the one requested"); thread = FindThread(threadReference); - if (thread != null) - { - thread.SetActive(); - } + thread?.SetActive(); } - var stackFrames = new List(); + var stackFrames = new List(); var totalFrames = 0; var bt = thread.Backtrace; @@ -612,11 +706,18 @@ public override void StackTrace(Response response, dynamic arguments) var frameHandle = m_FrameHandles.Create(frame); string name = frame.SourceLocation.MethodName; int line = frame.SourceLocation.Line; - stackFrames.Add(new UnityDebugAdapter.StackFrame(frameHandle, name, source, ConvertDebuggerLineToClient(line), 0, hint)); + stackFrames.Add(new StackFrame(frameHandle, name, source, ConvertDebuggerLineToClient(line), 0, hint)); } } - SendResponse(response, new StackTraceResponseBody(stackFrames, totalFrames)); + var response = new Response() + { + command = "stackTrace", + request_seq = reqSeq, + success = true, + body = new StackTraceResponseBody(stackFrames, totalFrames), + }; + SendMessage(response); } ThreadInfo DebuggerActiveThread() @@ -627,14 +728,16 @@ ThreadInfo DebuggerActiveThread() } } - public override void Source(Response response, dynamic arguments) + public override void Source(int reqSeq, JToken args) { - SendErrorResponse(response, 1020, "No source available"); + SendErrorResponse(reqSeq, "source", 1020, "No source available"); } - public override void Scopes(Response response, dynamic args) + public override void Scopes(int reqSeq, JToken args) { - int frameId = GetInt(args, "frameId", 0); + var _args = args.ToObject(); + + int frameId = _args.frameId; var frame = m_FrameHandles.Get(frameId, null); var scopes = new List(); @@ -650,15 +753,24 @@ public override void Scopes(Response response, dynamic args) scopes.Add(new Scope("Local", m_VariableHandles.Create(locals))); } - SendResponse(response, new ScopesResponseBody(scopes)); + var response = new Response() + { + command = "scopes", + request_seq = reqSeq, + success = true, + body = new ScopesResponseBody(scopes), + }; + SendMessage(response); } - public override void Variables(Response response, dynamic args) + public override void Variables(int reqSeq, JToken args) { - int reference = GetInt(args, "variablesReference", -1); + var _args = args.ToObject(); + int reference = _args.variablesReference; if (reference == -1) { - SendErrorResponse(response, 3009, "variables: property 'variablesReference' is missing", null, false, true); + SendErrorResponse(reqSeq, "variables", 3009, + "variables: property 'variablesReference' is missing", null, false, true); return; } @@ -700,10 +812,18 @@ public override void Variables(Response response, dynamic args) } } - SendResponse(response, new VariablesResponseBody(variables)); + + var response = new Response() + { + command = "variables", + request_seq = reqSeq, + success = true, + body = new VariablesResponseBody(variables), + }; + SendMessage(response); } - public override void Threads(Response response, dynamic args) + public override void Threads(int reqSeq, JToken args) { var threads = new List(); var process = m_ActiveProcess; @@ -724,33 +844,51 @@ public override void Threads(Response response, dynamic args) threads = d.Values.ToList(); } - SendResponse(response, new ThreadsResponseBody(threads)); + var response = new Response() + { + command = "threads", + request_seq = reqSeq, + success = true, + body = new ThreadsResponseBody(threads), + }; + SendMessage(response); } - public override void Evaluate(Response response, dynamic args) + public override void Evaluate(int reqSeq, JToken args) { - var expression = GetString(args, "expression"); - var frameId = GetInt(args, "frameId", 0); + var _args = args.ToObject(); + var expression = _args.expression; + var frameId = _args.frameId ?? 0; if (expression == null) { - SendError(response, "expression missing"); + SendErrorResponse(reqSeq, "evaluate", 3014, "Evaluate request failed ({_reason}).", + new Dictionary { { "_reason", "expression missing" } }); return; } var frame = m_FrameHandles.Get(frameId, null); if (frame == null) { - SendError(response, "no active stackframe"); + SendErrorResponse(reqSeq, "evaluate", 3014, "Evaluate request failed ({_reason}).", + new Dictionary { { "_reason", "no active stackframe" } }); return; } if (!frame.ValidateExpression(expression)) { - SendError(response, "invalid expression"); + SendErrorResponse(reqSeq, "evaluate", 3014, "Evaluate request failed ({_reason}).", + new Dictionary { { "_reason", "invalid expression" } }); return; } + var response = new Response() + { + command = "evaluate", + request_seq = reqSeq, + success = true, + }; + var evaluationOptions = m_DebuggerSessionOptions.EvaluationOptions.Clone(); evaluationOptions.EllipsizeStrings = false; evaluationOptions.AllowMethodEvaluation = true; @@ -766,19 +904,22 @@ public override void Evaluate(Response response, dynamic args) error = "not available"; } - SendResponse(response, new EvaluateResponseBody(error)); + response.body = new EvaluateResponseBody(error); + SendMessage(response); return; } if (flags.HasFlag(ObjectValueFlags.Unknown)) { - SendResponse(response, new EvaluateResponseBody("invalid expression")); + response.body = new EvaluateResponseBody("invalid expression"); + SendMessage(response); return; } if (flags.HasFlag(ObjectValueFlags.Object) && flags.HasFlag(ObjectValueFlags.Namespace)) { - SendResponse(response, new EvaluateResponseBody("not available")); + response.body = new EvaluateResponseBody("not available"); + SendMessage(response); return; } @@ -788,13 +929,10 @@ public override void Evaluate(Response response, dynamic args) handle = m_VariableHandles.Create(val.GetAllChildren()); } - SendResponse(response, new EvaluateResponseBody(val.DisplayValue, handle)); + response.body = new EvaluateResponseBody(val.DisplayValue, handle); + SendMessage(response); } - void SendError(Response response, string error) - { - SendErrorResponse(response, 3014, "Evaluate request failed ({_reason}).", new { _reason = error }); - } //---- private ------------------------------------------ @@ -868,7 +1006,7 @@ Variable CreateVariable(ObjectValue v) Backtrace DebuggerActiveBacktrace() { var thr = DebuggerActiveThread(); - return thr == null ? null : thr.Backtrace; + return thr?.Backtrace; } ExceptionInfo DebuggerActiveException() @@ -882,48 +1020,6 @@ bool HasMonoExtension(string path) return MONO_EXTENSIONS.Any(path.EndsWith); } - static int GetInt(dynamic args, string property, int dflt = 0) - { - try - { - return (int)args[property]; - } - catch (Exception) - { - // this happens so often that it fills up the log fast => hence the LogDebug and not LogWarn - Logger.LogDebug($"could not GetInt from dynamic container: {JsonConvert.SerializeObject(args)} at property: {property}"); - // ignore and return default value - } - - return dflt; - } - - static string GetString(dynamic args, string property, string dflt = null) - { - var s = (string)args[property]; - if (s == null) - { - // this happens so often that it fills up the log fast => hence the LogDebug and not LogWarn - Logger.LogDebug($"could not GetString from dynamic args: {JsonConvert.SerializeObject(args)} at property: {property}"); - return dflt; - } - - s = s.Trim(); - if (s.Length == 0) - { - return dflt; - } - - return s; - } - - static SourceBreakpoint[] getBreakpoints(dynamic args, string property) - { - JArray jsonBreakpoints = args[property]; - var breakpoints = jsonBreakpoints.ToObject(); - return breakpoints ?? new SourceBreakpoint[0]; - } - void DebuggerKill() { lock (m_Lock) diff --git a/unity-debug-adapter/Utilities.cs b/unity-debug-adapter/Utilities.cs deleted file mode 100644 index 018ee86..0000000 --- a/unity-debug-adapter/Utilities.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using System.Reflection; - -namespace UnityDebugAdapter -{ - public class Utilities - { - private static readonly Regex VARIABLE = new Regex(@"\{(\w+)\}"); - - public static string ExpandVariables(string format, dynamic variables, bool underscoredOnly = true) - { - if (variables == null) - { - variables = new { }; - } - Type type = variables.GetType(); - return VARIABLE.Replace(format, match => - { - string name = match.Groups[1].Value; - if (!underscoredOnly || name.StartsWith("_")) - { - - PropertyInfo property = type.GetProperty(name); - if (property != null) - { - object value = property.GetValue(variables, null); - return value.ToString(); - } - return '{' + name + ": not found}"; - } - return match.Groups[0].Value; - }); - } - } -} diff --git a/unity-debug-adapter/unity-debug-adapter.csproj b/unity-debug-adapter/unity-debug-adapter.csproj index d9da1c4..9de971c 100644 --- a/unity-debug-adapter/unity-debug-adapter.csproj +++ b/unity-debug-adapter/unity-debug-adapter.csproj @@ -5,6 +5,7 @@ net472 false ../bin/$(Configuration) + 8.0 @@ -19,4 +20,18 @@ + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + +