diff --git a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs index 5c46bc7324..1932bc2044 100644 --- a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs +++ b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs @@ -11,19 +11,20 @@ using Newtonsoft.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Threading; +using Microsoft.Extensions.Logging; namespace WebAssembly.Net.Debugging { - internal class BreakPointRequest { + internal class BreakpointRequest { public string Assembly { get; private set; } public string File { get; private set; } public int Line { get; private set; } public int Column { get; private set; } public override string ToString () { - return $"BreakPointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}"; + return $"BreakpointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}"; } - public static BreakPointRequest Parse (JObject args, DebugStore store) + public static BreakpointRequest Parse (JObject args, DebugStore store) { // Events can potentially come out of order, so DebugStore may not be initialized // The BP being set in these cases are JS ones, which we can safely ignore @@ -38,7 +39,7 @@ namespace WebAssembly.Net.Debugging { url = sourceFile?.DotNetUrl; } - if (url != null && !url.StartsWith ("dotnet://", StringComparison.InvariantCulture)) { + if (url != null && !url.StartsWith ("dotnet://", StringComparison.Ordinal)) { var sourceFile = store.GetFileByUrl (url); url = sourceFile?.DotNetUrl; } @@ -55,7 +56,7 @@ namespace WebAssembly.Net.Debugging { if (line == null || column == null) return null; - return new BreakPointRequest () { + return new BreakpointRequest () { Assembly = parts.Assembly, File = parts.DocumentPath, Line = line.Value, @@ -147,7 +148,9 @@ namespace WebAssembly.Net.Debugging { if (obj == null) return null; - var id = SourceId.TryParse (obj ["scriptId"]?.Value ()); + if (!SourceId.TryParse (obj ["scriptId"]?.Value (), out var id)) + return null; + var line = obj ["lineNumber"]?.Value (); var column = obj ["columnNumber"]?.Value (); if (id == null || line == null || column == null) @@ -156,18 +159,17 @@ namespace WebAssembly.Net.Debugging { return new SourceLocation (id, line.Value, column.Value); } - internal JObject ToJObject () - { - return JObject.FromObject (new { + internal object AsLocation () + => new { scriptId = id.ToString (), lineNumber = line, columnNumber = column - }); - } - + }; } internal class SourceId { + const string Scheme = "dotnet://"; + readonly int assembly, document; public int Assembly => assembly; @@ -179,25 +181,27 @@ namespace WebAssembly.Net.Debugging { this.document = document; } - public SourceId (string id) { - id = id.Substring ("dotnet://".Length); + id = id.Substring (Scheme.Length); var sp = id.Split ('_'); this.assembly = int.Parse (sp [0]); this.document = int.Parse (sp [1]); } - public static SourceId TryParse (string id) + public static bool TryParse (string id, out SourceId script) { - if (!id.StartsWith ("dotnet://", StringComparison.InvariantCulture)) - return null; - return new SourceId (id); + script = null; + if (id == null || !id.StartsWith (Scheme, StringComparison.Ordinal)) + return false; + script = new SourceId (id); + return true; } + public override string ToString () { - return $"dotnet://{assembly}_{document}"; + return $"{Scheme}{assembly}_{document}"; } public override bool Equals (object obj) @@ -289,6 +293,7 @@ namespace WebAssembly.Net.Debugging { static int next_id; ModuleDefinition image; readonly int id; + readonly ILogger logger; Dictionary methods = new Dictionary (); Dictionary sourceLinkMappings = new Dictionary(); readonly List sources = new List(); @@ -296,9 +301,7 @@ namespace WebAssembly.Net.Debugging { public AssemblyInfo (string url, byte[] assembly, byte[] pdb) { - lock (typeof (AssemblyInfo)) { - this.id = ++next_id; - } + this.id = Interlocked.Increment (ref next_id); try { Url = url; @@ -313,7 +316,7 @@ namespace WebAssembly.Net.Debugging { this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp); } catch (BadImageFormatException ex) { - Console.WriteLine ($"Failed to read assembly as portable PDB: {ex.Message}"); + logger.LogWarning ($"Failed to read assembly as portable PDB: {ex.Message}"); } catch (ArgumentNullException) { if (pdb != null) throw; @@ -336,8 +339,9 @@ namespace WebAssembly.Net.Debugging { Populate (); } - public AssemblyInfo () + public AssemblyInfo (ILogger logger) { + this.logger = logger; } void Populate () @@ -360,7 +364,7 @@ namespace WebAssembly.Net.Debugging { foreach (var m in image.GetTypes().SelectMany(t => t.Methods)) { Document first_doc = null; foreach (var sp in m.DebugInformation.SequencePoints) { - if (first_doc == null && !sp.Document.Url.EndsWith (".g.cs")) { + if (first_doc == null && !sp.Document.Url.EndsWith (".g.cs", StringComparison.OrdinalIgnoreCase)) { first_doc = sp.Document; } // else if (first_doc != sp.Document) { @@ -473,29 +477,61 @@ namespace WebAssembly.Net.Debugging { } else { this.Url = DotNetUrl; } - } internal void AddMethod (MethodInfo mi) { this.methods.Add (mi); } + public string DebuggerFileName { get; } public string Url { get; } public string AssemblyName => assembly.Name; public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}"; - public string DocHashCode => "abcdee" + id; + public SourceId SourceId => new SourceId (assembly.Id, this.id); public Uri SourceLinkUri { get; } public Uri SourceUri { get; } public IEnumerable Methods => this.methods; + public byte[] EmbeddedSource => doc.EmbeddedSource; + + public (int startLine, int startColumn, int endLine, int endColumn) GetExtents () + { + var start = methods.OrderBy (m => m.StartLocation.Line).ThenBy (m => m.StartLocation.Column).First (); + var end = methods.OrderByDescending (m => m.EndLocation.Line).ThenByDescending (m => m.EndLocation.Column).First (); + return (start.StartLocation.Line, start.StartLocation.Column, end.EndLocation.Line, end.EndLocation.Column); + } + + public async Task LoadSource () + { + if (EmbeddedSource.Length > 0) + return await Task.FromResult (EmbeddedSource); + + return null; + } + + public object ToScriptSource (int executionContextId, object executionContextAuxData) + { + return new { + scriptId = SourceId.ToString (), + url = Url, + executionContextId, + executionContextAuxData, + //hash = "abcdee" + id, + dotNetUrl = DotNetUrl, + }; + } } internal class DebugStore { - // MonoProxy proxy; - commenting out because never gets assigned List assemblies = new List (); HttpClient client = new HttpClient (); + readonly ILogger logger; + + public DebugStore (ILogger logger) { + this.logger = logger; + } class DebugItem { public string Url { get; set; } @@ -526,16 +562,7 @@ namespace WebAssembly.Net.Debugging { Data = Task.WhenAll (client.GetByteArrayAsync (url), pdb != null ? client.GetByteArrayAsync (pdb) : Task.FromResult (null)) }); } catch (Exception e) { - Console.WriteLine ($"Failed to read {url} ({e.Message})"); - var o = JObject.FromObject (new { - entry = new { - source = "other", - level = "warning", - text = $"Failed to read {url} ({e.Message})" - } - }); - // proxy.SendEvent (sessionId, "Log.entryAdded", o, token); - commenting out because `proxy` would always be null - + logger.LogDebug ($"Failed to read {url} ({e.Message})"); } } @@ -544,7 +571,7 @@ namespace WebAssembly.Net.Debugging { var bytes = await step.Data; assemblies.Add (new AssemblyInfo (step.Url, bytes[0], bytes[1])); } catch (Exception e) { - Console.WriteLine ($"Failed to Load {step.Url} ({e.Message})"); + logger.LogDebug ($"Failed to Load {step.Url} ({e.Message})"); } } } @@ -592,8 +619,7 @@ namespace WebAssembly.Net.Debugging { var res = new List (); if (doc == null) { - //FIXME we need to write up logging here - Console.WriteLine ($"Could not find document {src_id}"); + logger.LogDebug ($"Could not find document {src_id}"); return res; } @@ -630,7 +656,7 @@ namespace WebAssembly.Net.Debugging { return true; } - public SourceLocation FindBestBreakpoint (BreakPointRequest req) + public SourceLocation FindBestBreakpoint (BreakpointRequest req) { var asm = assemblies.FirstOrDefault (a => a.Name.Equals (req.Assembly, StringComparison.OrdinalIgnoreCase)); var src = asm?.Sources?.FirstOrDefault (s => s.DebuggerFileName.Equals (req.File, StringComparison.OrdinalIgnoreCase)); diff --git a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsClient.cs b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsClient.cs index 3fa95ef346..4fb0fd0f86 100644 --- a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsClient.cs +++ b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsClient.cs @@ -6,6 +6,7 @@ using System.Threading; using System.IO; using System.Text; using System.Collections.Generic; +using Microsoft.Extensions.Logging; namespace WebAssembly.Net.Debugging { internal class DevToolsClient: IDisposable { @@ -14,8 +15,10 @@ namespace WebAssembly.Net.Debugging { TaskCompletionSource side_exit = new TaskCompletionSource (); List pending_writes = new List (); Task current_write; + readonly ILogger logger; - public DevToolsClient () { + public DevToolsClient (ILogger logger) { + this.logger = logger; } ~DevToolsClient() { @@ -99,7 +102,7 @@ namespace WebAssembly.Net.Debugging { Func send, CancellationToken token) { - Console.WriteLine ("connecting to {0}", uri); + logger.LogDebug ("connecting to {0}", uri); this.socket = new ClientWebSocket (); this.socket.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan; diff --git a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs index e2e24eff79..158fdcc1df 100644 --- a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs +++ b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; @@ -28,8 +28,17 @@ namespace WebAssembly.Net.Debugging { Result (JObject result, JObject error) { - this.Value = result; - this.Error = error; + if (result != null && error != null) + throw new ArgumentException ($"Both {nameof(result)} and {nameof(error)} arguments cannot be non-null."); + + bool resultHasError = String.Compare ((result? ["result"] as JObject)? ["subtype"]?. Value (), "error") == 0; + if (result != null && resultHasError) { + this.Value = null; + this.Error = result; + } else { + this.Value = result; + this.Error = error; + } } public static Result FromJson (JObject obj) @@ -39,14 +48,16 @@ namespace WebAssembly.Net.Debugging { } public static Result Ok (JObject ok) - { - return new Result (ok, null); - } + => new Result (ok, null); + + public static Result OkFromObject (object ok) + => Ok (JObject.FromObject(ok)); public static Result Err (JObject err) - { - return new Result (null, err); - } + => new Result (null, err); + + public static Result Exception (Exception e) + => new Result (null, JObject.FromObject (new { message = e.Message })); public JObject ToJObject (MessageId target) { if (IsOk) { @@ -83,7 +94,7 @@ namespace WebAssembly.Net.Debugging { if (pending.Count == 1) { if (current_send != null) throw new Exception ("current_send MUST BE NULL IF THERE'S no pending send"); - //Console.WriteLine ("sending {0} bytes", bytes.Length); + //logger.LogTrace ("sending {0} bytes", bytes.Length); current_send = Ws.SendAsync (new ArraySegment (bytes), WebSocketMessageType.Text, true, token); return current_send; } @@ -98,7 +109,7 @@ namespace WebAssembly.Net.Debugging { if (pending.Count > 0) { if (current_send != null) throw new Exception ("current_send MUST BE NULL IF THERE'S no pending send"); - //Console.WriteLine ("sending more {0} bytes", pending[0].Length); + current_send = Ws.SendAsync (new ArraySegment (pending [0]), WebSocketMessageType.Text, true, token); return current_send; } @@ -115,12 +126,12 @@ namespace WebAssembly.Net.Debugging { int next_cmd_id; List pending_ops = new List (); List queues = new List (); - readonly ILogger logger; + protected readonly ILogger logger; - public DevToolsProxy(ILoggerFactory loggerFactory) - { - logger = loggerFactory.CreateLogger(); - } + public DevToolsProxy (ILoggerFactory loggerFactory) + { + logger = loggerFactory.CreateLogger(); + } protected virtual Task AcceptEvent (SessionId sessionId, string method, JObject args, CancellationToken token) { @@ -184,7 +195,7 @@ namespace WebAssembly.Net.Debugging { { try { if (!await AcceptEvent (sessionId, method, args, token)) { - //Console.WriteLine ("proxy browser: {0}::{1}",method, args); + //logger.LogDebug ("proxy browser: {0}::{1}",method, args); SendEventInternal (sessionId, method, args, token); } } catch (Exception e) { @@ -206,7 +217,7 @@ namespace WebAssembly.Net.Debugging { void OnResponse (MessageId id, Result result) { - //Console.WriteLine ("got id {0} res {1}", id, result); + //logger.LogTrace ("got id {0} res {1}", id, result); // Fixme var idx = pending_cmds.FindIndex (e => e.Item1.id == id.id && e.Item1.sessionId == id.sessionId); var item = pending_cmds [idx]; @@ -242,7 +253,7 @@ namespace WebAssembly.Net.Debugging { Task SendCommandInternal (SessionId sessionId, string method, JObject args, CancellationToken token) { - int id = ++next_cmd_id; + int id = Interlocked.Increment (ref next_cmd_id); var o = JObject.FromObject (new { sessionId.sessionId, @@ -252,7 +263,6 @@ namespace WebAssembly.Net.Debugging { }); var tcs = new TaskCompletionSource (); - var msgId = new MessageId { id = id, sessionId = sessionId.sessionId }; //Log ("verbose", $"add cmd id {sessionId}-{id}"); pending_cmds.Add ((msgId , tcs)); @@ -314,7 +324,7 @@ namespace WebAssembly.Net.Debugging { try { while (!x.IsCancellationRequested) { var task = await Task.WhenAny (pending_ops.ToArray ()); - //Console.WriteLine ("pump {0} {1}", task, pending_ops.IndexOf (task)); + //logger.LogTrace ("pump {0} {1}", task, pending_ops.IndexOf (task)); if (task == pending_ops [0]) { var msg = ((Task)task).Result; if (msg != null) { @@ -360,23 +370,17 @@ namespace WebAssembly.Net.Debugging { { switch (priority) { case "protocol": - logger.LogTrace (msg); + //logger.LogTrace (msg); break; case "verbose": - logger.LogDebug (msg); + //logger.LogDebug (msg); break; case "info": - logger.LogInformation(msg); - break; case "warning": - logger.LogWarning(msg); - break; - case "error": - logger.LogError (msg); - break; + case "error": default: - logger.LogError(msg); - break; + logger.LogDebug (msg); + break; } } } diff --git a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs index e74d468494..c38705692c 100644 --- a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs +++ b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs @@ -12,16 +12,44 @@ using Microsoft.Extensions.Logging; namespace WebAssembly.Net.Debugging { internal class MonoCommands { - public const string GET_CALL_STACK = "MONO.mono_wasm_get_call_stack()"; - public const string IS_RUNTIME_READY_VAR = "MONO.mono_wasm_runtime_is_ready"; - public const string START_SINGLE_STEPPING = "MONO.mono_wasm_start_single_stepping({0})"; - public const string GET_SCOPE_VARIABLES = "MONO.mono_wasm_get_variables({0}, [ {1} ])"; - public const string SET_BREAK_POINT = "MONO.mono_wasm_set_breakpoint(\"{0}\", {1}, {2})"; - public const string REMOVE_BREAK_POINT = "MONO.mono_wasm_remove_breakpoint({0})"; - public const string GET_LOADED_FILES = "MONO.mono_wasm_get_loaded_files()"; - public const string CLEAR_ALL_BREAKPOINTS = "MONO.mono_wasm_clear_all_breakpoints()"; - public const string GET_OBJECT_PROPERTIES = "MONO.mono_wasm_get_object_properties({0})"; - public const string GET_ARRAY_VALUES = "MONO.mono_wasm_get_array_values({0})"; + public string expression { get; set; } + public string objectGroup { get; set; } = "mono-debugger"; + public bool includeCommandLineAPI { get; set; } = false; + public bool silent { get; set; } = false; + public bool returnByValue { get; set; } = true; + + public MonoCommands (string expression) + => this.expression = expression; + + public static MonoCommands GetCallStack () + => new MonoCommands ("MONO.mono_wasm_get_call_stack()"); + + public static MonoCommands IsRuntimeReady () + => new MonoCommands ("MONO.mono_wasm_runtime_is_ready"); + + public static MonoCommands StartSingleStepping (StepKind kind) + => new MonoCommands ($"MONO.mono_wasm_start_single_stepping ({(int)kind})"); + + public static MonoCommands GetLoadedFiles () + => new MonoCommands ("MONO.mono_wasm_get_loaded_files()"); + + public static MonoCommands ClearAllBreakpoints () + => new MonoCommands ("MONO.mono_wasm_clear_all_breakpoints()"); + + public static MonoCommands GetObjectProperties (int objectId) + => new MonoCommands ($"MONO.mono_wasm_get_object_properties({objectId})"); + + public static MonoCommands GetArrayValues (int objectId) + => new MonoCommands ($"MONO.mono_wasm_get_array_values({objectId})"); + + public static MonoCommands GetScopeVariables (int scopeId, params int[] vars) + => new MonoCommands ($"MONO.mono_wasm_get_variables({scopeId}, [ {string.Join (",", vars)} ])"); + + public static MonoCommands SetBreakpoint (string assemblyName, int methodToken, int ilOffset) + => new MonoCommands ($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})"); + + public static MonoCommands RemoveBreakpoint (int breakpointId) + => new MonoCommands ($"MONO.mono_wasm_remove_breakpoint({breakpointId})"); } internal enum MonoErrorCodes { @@ -45,14 +73,23 @@ namespace WebAssembly.Net.Debugging { public int Id { get; private set; } } - class Breakpoint { public SourceLocation Location { get; private set; } public int LocalId { get; private set; } public int RemoteId { get; set; } - public BreakPointState State { get; set; } + public BreakpointState State { get; set; } + public string StackId => $"dotnet:{LocalId}"; - public Breakpoint (SourceLocation loc, int localId, BreakPointState state) + public static bool TryParseId (string stackId, out int id) + { + id = -1; + if (stackId?.StartsWith ("dotnet:", StringComparison.Ordinal) != true) + return false; + + return int.TryParse (stackId.Substring ("dotnet:".Length), out id); + } + + public Breakpoint (SourceLocation loc, int localId, BreakpointState state) { this.Location = loc; this.LocalId = localId; @@ -60,7 +97,7 @@ namespace WebAssembly.Net.Debugging { } } - enum BreakPointState { + enum BreakpointState { Active, Disabled, Pending @@ -72,54 +109,92 @@ namespace WebAssembly.Net.Debugging { Over } + internal class ExecutionContext { + int breakpointIndex = -1; + public List Breakpoints { get; } = new List (); + + public bool RuntimeReady { get; set; } + public int Id { get; set; } + public object AuxData { get; set; } + + public List CallStack { get; set; } + + public int NextBreakpointId () + => Interlocked.Increment (ref breakpointIndex); + + internal DebugStore store; + public TaskCompletionSource Source { get; } = new TaskCompletionSource (); + + public DebugStore Store { + get { + if (store == null || !Source.Task.IsCompleted) + return null; + + return store; + } + } + } + internal class MonoProxy : DevToolsProxy { - DebugStore store; - List breakpoints = new List (); - List current_callstack; - bool runtime_ready; - int local_breakpoint_id; - int ctx_id; - JObject aux_ctx_data; + Dictionary contexts = new Dictionary (); public MonoProxy (ILoggerFactory loggerFactory) : base(loggerFactory) { } + ExecutionContext GetContext (SessionId sessionId) + { + var id = sessionId?.sessionId ?? "default"; + if (contexts.TryGetValue (id, out var context)) + return context; + + throw new ArgumentException ($"Invalid Session: \"{id}\"", nameof (sessionId)); + } + + internal Task SendMonoCommand (SessionId id, MonoCommands cmd, CancellationToken token) + => SendCommand (id, "Runtime.evaluate", JObject.FromObject (cmd), token); + protected override async Task AcceptEvent (SessionId sessionId, string method, JObject args, CancellationToken token) { switch (method) { case "Runtime.executionContextCreated": { + SendEvent (sessionId, method, args, token); var ctx = args? ["context"]; var aux_data = ctx? ["auxData"] as JObject; + var id = ctx ["id"].Value (); if (aux_data != null) { var is_default = aux_data ["isDefault"]?.Value (); if (is_default == true) { - var id = new MessageId { id = ctx ["id"].Value (), sessionId = sessionId.sessionId }; - await OnDefaultContext (id, aux_data, token); + await OnDefaultContext (sessionId, new ExecutionContext { Id = id, AuxData = aux_data }, token); } } - break; + return true; + //break; } + case "Debugger.paused": { //TODO figure out how to stich out more frames and, in particular what happens when real wasm is on the stack var top_func = args? ["callFrames"]? [0]? ["functionName"]?.Value (); + if (top_func == "mono_wasm_fire_bp" || top_func == "_mono_wasm_fire_bp") { - await OnBreakPointHit (sessionId, args, token); - return true; + return await OnBreakpointHit (sessionId, args, token); } if (top_func == MonoConstants.RUNTIME_IS_READY) { - await OnRuntimeReady (new SessionId { sessionId = sessionId.sessionId }, token); + await OnRuntimeReady (sessionId, token); return true; } break; } + case "Debugger.scriptParsed":{ - if (args?["url"]?.Value ()?.StartsWith ("wasm://") == true) { - // Console.WriteLine ("ignoring wasm event"); - return true; + var url = args? ["url"]?.Value () ?? ""; + + switch (url) { + case var _ when url == "": + case var _ when url.StartsWith ("wasm://", StringComparison.Ordinal): { + Log ("info", $"ignoring wasm: Debugger.scriptParsed {url}"); + return true; + } } - break; - } - case "Debugger.enabled": { - await LoadStore (new SessionId { sessionId = args? ["sessionId"]?.Value () }, token); + Log ("info", $"proxying Debugger.scriptParsed ({sessionId.sessionId}) {url} {args}"); break; } } @@ -130,24 +205,15 @@ namespace WebAssembly.Net.Debugging { protected override async Task AcceptCommand (MessageId id, string method, JObject args, CancellationToken token) { switch (method) { - case "Target.attachToTarget": { - break; - } - case "Target.attachToBrowserTarget": { - break; - } + case "Debugger.getScriptSource": { - var script_id = args? ["scriptId"]?.Value (); - if (script_id.StartsWith ("dotnet://", StringComparison.InvariantCultureIgnoreCase)) { - await OnGetScriptSource (id, script_id, token); - return true; - } - break; + var script = args? ["scriptId"]?.Value (); + return await OnGetScriptSource (id, script, token); } case "Runtime.compileScript": { var exp = args? ["expression"]?.Value (); - if (exp.StartsWith ("//dotnet:", StringComparison.InvariantCultureIgnoreCase)) { + if (exp.StartsWith ("//dotnet:", StringComparison.Ordinal)) { OnCompileDotnetScript (id, token); return true; } @@ -165,7 +231,7 @@ namespace WebAssembly.Net.Debugging { case "Debugger.setBreakpointByUrl": { Log ("info", $"BP req {args}"); - var bp_req = BreakPointRequest.Parse (args, store); + var bp_req = BreakpointRequest.Parse (args, GetContext (id).Store); if (bp_req != null) { await SetBreakPoint (id, bp_req, token); return true; @@ -178,37 +244,25 @@ namespace WebAssembly.Net.Debugging { } case "Debugger.resume": { - await OnResume (token); + await OnResume (id, token); break; } case "Debugger.stepInto": { - if (this.current_callstack != null) { - await Step (id, StepKind.Into, token); - return true; - } - break; + return await Step (id, StepKind.Into, token); } case "Debugger.stepOut": { - if (this.current_callstack != null) { - await Step (id, StepKind.Out, token); - return true; - } - break; + return await Step (id, StepKind.Out, token); } case "Debugger.stepOver": { - if (this.current_callstack != null) { - await Step (id, StepKind.Over, token); - return true; - } - break; + return await Step (id, StepKind.Over, token); } case "Runtime.getProperties": { var objId = args? ["objectId"]?.Value (); - if (objId.StartsWith ("dotnet:")) { + if (objId.StartsWith ("dotnet:", StringComparison.Ordinal)) { var parts = objId.Split (new char [] { ':' }); if (parts.Length < 3) return true; @@ -218,11 +272,11 @@ namespace WebAssembly.Net.Debugging { break; } case "object": { - await GetDetails (id, int.Parse (parts[2]), token, MonoCommands.GET_OBJECT_PROPERTIES); + await GetDetails (id, MonoCommands.GetObjectProperties (int.Parse (parts[2])), token); break; } case "array": { - await GetDetails (id, int.Parse (parts[2]), token, MonoCommands.GET_ARRAY_VALUES); + await GetDetails (id, MonoCommands.GetArrayValues (int.Parse (parts [2])), token); break; } } @@ -237,39 +291,30 @@ namespace WebAssembly.Net.Debugging { async Task OnRuntimeReady (SessionId sessionId, CancellationToken token) { - Log ("info", "RUNTIME READY, PARTY TIME"); + Log ("info", "Runtime ready"); await RuntimeReady (sessionId, token); await SendCommand (sessionId, "Debugger.resume", new JObject (), token); SendEvent (sessionId, "Mono.runtimeReady", new JObject (), token); } //static int frame_id=0; - async Task OnBreakPointHit (SessionId sessionId, JObject args, CancellationToken token) + async Task OnBreakpointHit (SessionId sessionId, JObject args, CancellationToken token) { //FIXME we should send release objects every now and then? Or intercept those we inject and deal in the runtime - var o = JObject.FromObject (new { - expression = MonoCommands.GET_CALL_STACK, - objectGroup = "mono_debugger", - includeCommandLineAPI = false, - silent = false, - returnByValue = true - }); - + var res = await SendMonoCommand (sessionId, MonoCommands.GetCallStack(), token); var orig_callframes = args? ["callFrames"]?.Values (); - var res = await SendCommand (sessionId, "Runtime.evaluate", o, token); + var context = GetContext (sessionId); if (res.IsErr) { //Give up and send the original call stack - SendEvent (sessionId, "Debugger.paused", args, token); - return; + return false; } //step one, figure out where did we hit var res_value = res.Value? ["result"]? ["value"]; if (res_value == null || res_value is JValue) { //Give up and send the original call stack - SendEvent (sessionId, "Debugger.paused", args, token); - return; + return false; } Log ("verbose", $"call stack (err is {res.Error} value is:\n{res.Value}"); @@ -277,14 +322,14 @@ namespace WebAssembly.Net.Debugging { Log ("verbose", $"We just hit bp {bp_id}"); if (!bp_id.HasValue) { //Give up and send the original call stack - SendEvent (sessionId, "Debugger.paused", args, token); - return; + return false; } - var bp = this.breakpoints.FirstOrDefault (b => b.RemoteId == bp_id.Value); + var bp = context.Breakpoints.FirstOrDefault (b => b.RemoteId == bp_id.Value); + var store = context.Store; var src = bp == null ? null : store.GetFileById (bp.Location.Id); - var callFrames = new List (); + var callFrames = new List (); foreach (var frame in orig_callframes) { var function_name = frame ["functionName"]?.Value (); var url = frame ["url"]?.Value (); @@ -325,12 +370,12 @@ namespace WebAssembly.Net.Debugging { Log ("info", $"\tmethod {method.Name} location: {location}"); frames.Add (new Frame (method, location, frame_id)); - callFrames.Add (JObject.FromObject (new { + callFrames.Add (new { functionName = method.Name, callFrameId = $"dotnet:scope:{frame_id}", - functionLocation = method.StartLocation.ToJObject (), + functionLocation = method.StartLocation.AsLocation (), - location = location.ToJObject (), + location = location.AsLocation (), url = store.ToUrl (location), @@ -344,104 +389,92 @@ namespace WebAssembly.Net.Debugging { objectId = $"dotnet:scope:{frame_id}", }, name = method.Name, - startLocation = method.StartLocation.ToJObject (), - endLocation = method.EndLocation.ToJObject (), + startLocation = method.StartLocation.AsLocation (), + endLocation = method.EndLocation.AsLocation (), }} - })); + }); ++frame_id; - this.current_callstack = frames; + context.CallStack = frames; } - } else if (!(function_name.StartsWith ("wasm-function", StringComparison.InvariantCulture) - || url.StartsWith ("wasm://wasm/", StringComparison.InvariantCulture))) { + } else if (!(function_name.StartsWith ("wasm-function", StringComparison.Ordinal) + || url.StartsWith ("wasm://wasm/", StringComparison.Ordinal))) { callFrames.Add (frame); } } var bp_list = new string [bp == null ? 0 : 1]; if (bp != null) - bp_list [0] = $"dotnet:{bp.LocalId}"; + bp_list [0] = bp.StackId; - o = JObject.FromObject (new { - callFrames = callFrames, + var o = JObject.FromObject (new { + callFrames, reason = "other", //other means breakpoint hitBreakpoints = bp_list, }); SendEvent (sessionId, "Debugger.paused", o, token); + return true; } - async Task OnDefaultContext (MessageId ctx_id, JObject aux_data, CancellationToken token) + async Task OnDefaultContext (SessionId sessionId, ExecutionContext context, CancellationToken token) { Log ("verbose", "Default context created, clearing state and sending events"); + contexts[sessionId.sessionId ?? "default"] = context; //reset all bps - foreach (var b in this.breakpoints){ - b.State = BreakPointState.Pending; + foreach (var b in context.Breakpoints){ + b.State = BreakpointState.Pending; } - this.runtime_ready = false; - var o = JObject.FromObject (new { - expression = MonoCommands.IS_RUNTIME_READY_VAR, - objectGroup = "mono_debugger", - includeCommandLineAPI = false, - silent = false, - returnByValue = true - }); - this.ctx_id = ctx_id.id; - this.aux_ctx_data = aux_data; - - Log ("verbose", "checking if the runtime is ready"); - var res = await SendCommand (ctx_id, "Runtime.evaluate", o, token); + Log ("info", "checking if the runtime is ready"); + var res = await SendMonoCommand (sessionId, MonoCommands.IsRuntimeReady (), token); var is_ready = res.Value? ["result"]? ["value"]?.Value (); //Log ("verbose", $"\t{is_ready}"); if (is_ready.HasValue && is_ready.Value == true) { - Log ("verbose", "RUNTIME LOOK READY. GO TIME!"); - await OnRuntimeReady (ctx_id, token); + Log ("info", "RUNTIME LOOK READY. GO TIME!"); + await RuntimeReady (sessionId, token); + SendEvent (sessionId, "Mono.runtimeReady", new JObject (), token); } } - - async Task OnResume (CancellationToken token) + async Task OnResume (MessageId msd_id, CancellationToken token) { //discard frames - this.current_callstack = null; + GetContext (msd_id).CallStack = null; await Task.CompletedTask; } - async Task Step (MessageId msg_id, StepKind kind, CancellationToken token) + async Task Step (MessageId msg_id, StepKind kind, CancellationToken token) { + var context = GetContext (msg_id); + if (context.CallStack == null) + return false; - var o = JObject.FromObject (new { - expression = string.Format (MonoCommands.START_SINGLE_STEPPING, (int)kind), - objectGroup = "mono_debugger", - includeCommandLineAPI = false, - silent = false, - returnByValue = true, - }); - - var res = await SendCommand (msg_id, "Runtime.evaluate", o, token); + var res = await SendMonoCommand (msg_id, MonoCommands.StartSingleStepping (kind), token); SendResponse (msg_id, Result.Ok (new JObject ()), token); - this.current_callstack = null; + context.CallStack = null; await SendCommand (msg_id, "Debugger.resume", new JObject (), token); + return true; } - async Task GetDetails(MessageId msg_id, int object_id, CancellationToken token, string command) + static string FormatFieldName (string name) { - var o = JObject.FromObject(new - { - expression = string.Format(command, object_id), - objectGroup = "mono_debugger", - includeCommandLineAPI = false, - silent = false, - returnByValue = true, - }); + if (name.Contains("k__BackingField", StringComparison.Ordinal)) { + return name.Replace("k__BackingField", "", StringComparison.Ordinal) + .Replace("<", "", StringComparison.Ordinal) + .Replace(">", "", StringComparison.Ordinal); + } + return name; + } - var res = await SendCommand(msg_id, "Runtime.evaluate", o, token); + async Task GetDetails(MessageId msg_id, MonoCommands cmd, CancellationToken token) + { + var res = await SendMonoCommand(msg_id, cmd, token); //if we fail we just buble that to the IDE (and let it panic over it) if (res.IsErr) @@ -459,12 +492,7 @@ namespace WebAssembly.Net.Debugging { // so skip returning variable values in that case. for (int i = 0; i < values.Length; i+=2) { - string fieldName = (string)values[i]["name"]; - if (fieldName.Contains("k__BackingField")){ - fieldName = fieldName.Replace("k__BackingField", ""); - fieldName = fieldName.Replace("<", ""); - fieldName = fieldName.Replace(">", ""); - } + string fieldName = FormatFieldName ((string)values[i]["name"]); var value = values [i + 1]? ["value"]; if (((string)value ["description"]) == null) value ["description"] = value ["value"]?.ToString (); @@ -475,57 +503,54 @@ namespace WebAssembly.Net.Debugging { })); } - o = JObject.FromObject(new + var response = JObject.FromObject(new { result = var_list }); + + SendResponse(msg_id, Result.Ok(response), token); } catch (Exception e) { Log ("verbose", $"failed to parse {res.Value} - {e.Message}"); + SendResponse(msg_id, Result.Exception(e), token); } - SendResponse(msg_id, Result.Ok(o), token); + } async Task GetScopeProperties (MessageId msg_id, int scope_id, CancellationToken token) { - var scope = this.current_callstack.FirstOrDefault (s => s.Id == scope_id); - var vars = scope.Method.GetLiveVarsAt (scope.Location.CliLocation.Offset); - - - var var_ids = string.Join (",", vars.Select (v => v.Index)); - - var o = JObject.FromObject (new { - expression = string.Format (MonoCommands.GET_SCOPE_VARIABLES, scope.Id, var_ids), - objectGroup = "mono_debugger", - includeCommandLineAPI = false, - silent = false, - returnByValue = true, - }); - - var res = await SendCommand (msg_id, "Runtime.evaluate", o, token); - - //if we fail we just buble that to the IDE (and let it panic over it) - if (res.IsErr) { - SendResponse (msg_id, res, token); - return; - } try { + var scope = GetContext (msg_id).CallStack.FirstOrDefault (s => s.Id == scope_id); + var vars = scope.Method.GetLiveVarsAt (scope.Location.CliLocation.Offset); + + var var_ids = vars.Select (v => v.Index).ToArray (); + var res = await SendMonoCommand (msg_id, MonoCommands.GetScopeVariables (scope.Id, var_ids), token); + + //if we fail we just buble that to the IDE (and let it panic over it) + if (res.IsErr) { + SendResponse (msg_id, res, token); + return; + } + var values = res.Value? ["result"]? ["value"]?.Values ().ToArray (); - var var_list = new List (); + if(values == null) + SendResponse (msg_id, Result.OkFromObject (new {result = Array.Empty ()}), token); + + var var_list = new List (); int i = 0; // Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously // results in a "Memory access out of bounds", causing 'values' to be null, // so skip returning variable values in that case. - while (values != null && i < vars.Length && i < values.Length) { + while (i < vars.Length && i < values.Length) { var value = values [i] ["value"]; if (((string)value ["description"]) == null) value ["description"] = value ["value"]?.ToString (); - var_list.Add (JObject.FromObject (new { + var_list.Add (new { name = vars [i].Name, value - })); + }); i++; } //Async methods are special in the way that local variables can be lifted to generated class fields @@ -540,19 +565,17 @@ namespace WebAssembly.Net.Debugging { if (((string)value ["description"]) == null) value ["description"] = value ["value"]?.ToString (); - var_list.Add (JObject.FromObject (new { + var_list.Add (new { name, value - })); + }); i = i + 2; } - o = JObject.FromObject (new { - result = var_list - }); - SendResponse (msg_id, Result.Ok (o), token); + + SendResponse (msg_id, Result.OkFromObject (new { result = var_list }), token); } catch (Exception exception) { Log ("verbose", $"Error resolving scope properties {exception.Message}"); - SendResponse (msg_id, res, token); + SendResponse (msg_id, Result.Exception (exception), token); } } @@ -562,80 +585,63 @@ namespace WebAssembly.Net.Debugging { var method_token = bp.Location.CliLocation.Method.Token; var il_offset = bp.Location.CliLocation.Offset; - var o = JObject.FromObject (new { - expression = string.Format (MonoCommands.SET_BREAK_POINT, asm_name, method_token, il_offset), - objectGroup = "mono_debugger", - includeCommandLineAPI = false, - silent = false, - returnByValue = true, - }); - - var res = await SendCommand (sessionId, "Runtime.evaluate", o, token); + var res = await SendMonoCommand (sessionId, MonoCommands.SetBreakpoint (asm_name, method_token, il_offset), token); var ret_code = res.Value? ["result"]? ["value"]?.Value (); if (ret_code.HasValue) { bp.RemoteId = ret_code.Value; - bp.State = BreakPointState.Active; + bp.State = BreakpointState.Active; //Log ("verbose", $"BP local id {bp.LocalId} enabled with remote id {bp.RemoteId}"); } return res; } - async Task LoadStore (SessionId sessionId, CancellationToken token) + async Task LoadStore (SessionId sessionId, CancellationToken token) { - var o = JObject.FromObject (new { - expression = MonoCommands.GET_LOADED_FILES, - objectGroup = "mono_debugger", - includeCommandLineAPI = false, - silent = false, - returnByValue = true, - }); + var context = GetContext (sessionId); - var loaded_pdbs = await SendCommand (sessionId, "Runtime.evaluate", o, token); - var the_value = loaded_pdbs.Value? ["result"]? ["value"]; - var the_pdbs = the_value?.ToObject (); + if (Interlocked.CompareExchange (ref context.store, new DebugStore (logger), null) != null) { + return await context.Source.Task; + } - store = new DebugStore (); - await store.Load(sessionId, the_pdbs, token); + try { + var loaded_pdbs = await SendMonoCommand (sessionId, MonoCommands.GetLoadedFiles(), token); + var the_value = loaded_pdbs.Value? ["result"]? ["value"]; + var the_pdbs = the_value?.ToObject (); + + await context.store.Load(sessionId, the_pdbs, token); + } catch (Exception e) { + context.Source.SetException (e); + } + + if (!context.Source.Task.IsCompleted) + context.Source.SetResult (context.store); + return await context.Source.Task; } async Task RuntimeReady (SessionId sessionId, CancellationToken token) { - if (store == null) - await LoadStore (sessionId, token); + var context = GetContext (sessionId); + if (context.RuntimeReady) + return; - foreach (var s in store.AllSources ()) { - var ok = JObject.FromObject (new { - scriptId = s.SourceId.ToString (), - url = s.Url, - executionContextId = this.ctx_id, - hash = s.DocHashCode, - executionContextAuxData = this.aux_ctx_data, - dotNetUrl = s.DotNetUrl, - }); - //Log ("verbose", $"\tsending {s.Url}"); - SendEvent (sessionId, "Debugger.scriptParsed", ok, token); - } - - var o = JObject.FromObject (new { - expression = MonoCommands.CLEAR_ALL_BREAKPOINTS, - objectGroup = "mono_debugger", - includeCommandLineAPI = false, - silent = false, - returnByValue = true, - }); - - var clear_result = await SendCommand (sessionId, "Runtime.evaluate", o, token); + var clear_result = await SendMonoCommand (sessionId, MonoCommands.ClearAllBreakpoints (), token); if (clear_result.IsErr) { Log ("verbose", $"Failed to clear breakpoints due to {clear_result}"); } + context.RuntimeReady = true; + var store = await LoadStore (sessionId, token); - runtime_ready = true; + foreach (var s in store.AllSources ()) { + var scriptSource = JObject.FromObject (s.ToScriptSource (context.Id, context.AuxData)); + Log ("verbose", $"\tsending {s.Url} {context.Id} {sessionId.sessionId}"); + SendEvent (sessionId, "Debugger.scriptParsed", scriptSource, token); + } - foreach (var bp in breakpoints) { - if (bp.State != BreakPointState.Pending) + foreach (var bp in context.Breakpoints) { + if (bp.State != BreakpointState.Pending) continue; var res = await EnableBreakPoint (sessionId, bp, token); var ret_code = res.Value? ["result"]? ["value"]?.Value (); @@ -644,60 +650,53 @@ namespace WebAssembly.Net.Debugging { if (!ret_code.HasValue) { //FIXME figure out how to inform the IDE of that. Log ("info", $"FAILED TO ENABLE BP {bp.LocalId}"); - bp.State = BreakPointState.Disabled; + bp.State = BreakpointState.Disabled; } } } async Task RemoveBreakpoint(MessageId msg_id, JObject args, CancellationToken token) { var bpid = args? ["breakpointId"]?.Value (); - if (bpid?.StartsWith ("dotnet:") != true) + + if (!Breakpoint.TryParseId (bpid, out var the_id)) return false; - var the_id = int.Parse (bpid.Substring ("dotnet:".Length)); - - var bp = breakpoints.FirstOrDefault (b => b.LocalId == the_id); + var context = GetContext (msg_id); + var bp = context.Breakpoints.FirstOrDefault (b => b.LocalId == the_id); if (bp == null) { Log ("info", $"Could not find dotnet bp with id {the_id}"); return false; } - breakpoints.Remove (bp); + context.Breakpoints.Remove (bp); //FIXME verify result (and log?) - var res = await RemoveBreakPoint (msg_id, bp, token); + var res = await RemoveBreakpoint (msg_id, bp, token); return true; } - async Task RemoveBreakPoint (SessionId sessionId, Breakpoint bp, CancellationToken token) + async Task RemoveBreakpoint (SessionId sessionId, Breakpoint bp, CancellationToken token) { - var o = JObject.FromObject (new { - expression = string.Format (MonoCommands.REMOVE_BREAK_POINT, bp.RemoteId), - objectGroup = "mono_debugger", - includeCommandLineAPI = false, - silent = false, - returnByValue = true, - }); - - var res = await SendCommand (sessionId, "Runtime.evaluate", o, token); + var res = await SendMonoCommand (sessionId, MonoCommands.RemoveBreakpoint (bp.RemoteId), token); var ret_code = res.Value? ["result"]? ["value"]?.Value (); if (ret_code.HasValue) { bp.RemoteId = -1; - bp.State = BreakPointState.Disabled; + bp.State = BreakpointState.Disabled; } return res; } - async Task SetBreakPoint (MessageId msg_id, BreakPointRequest req, CancellationToken token) + async Task SetBreakPoint (MessageId msg_id, BreakpointRequest req, CancellationToken token) { - var bp_loc = store?.FindBestBreakpoint (req); - Log ("info", $"BP request for '{req}' runtime ready {runtime_ready} location '{bp_loc}'"); + var context = GetContext (msg_id); + var bp_loc = context.Store.FindBestBreakpoint (req); + Log ("info", $"BP request for '{req}' runtime ready {context.RuntimeReady} location '{bp_loc}'"); if (bp_loc == null) { - Log ("info", $"Could not resolve breakpoint request: {req}"); + Log ("verbose", $"Could not resolve breakpoint request: {req}"); SendResponse (msg_id, Result.Err(JObject.FromObject (new { code = (int)MonoErrorCodes.BpNotFound, message = $"C# Breakpoint at {req} not found." @@ -706,10 +705,10 @@ namespace WebAssembly.Net.Debugging { } Breakpoint bp = null; - if (!runtime_ready) { - bp = new Breakpoint (bp_loc, local_breakpoint_id++, BreakPointState.Pending); + if (!context.RuntimeReady) { + bp = new Breakpoint (bp_loc, context.NextBreakpointId (), BreakpointState.Pending); } else { - bp = new Breakpoint (bp_loc, local_breakpoint_id++, BreakPointState.Disabled); + bp = new Breakpoint (bp_loc, context.NextBreakpointId (), BreakpointState.Disabled); var res = await EnableBreakPoint (msg_id, bp, token); var ret_code = res.Value? ["result"]? ["value"]?.Value (); @@ -721,106 +720,75 @@ namespace WebAssembly.Net.Debugging { } } - var locations = new List (); + context.Breakpoints.Add (bp); - locations.Add (JObject.FromObject (new { - scriptId = bp_loc.Id.ToString (), - lineNumber = bp_loc.Line, - columnNumber = bp_loc.Column - })); + var ok = new { + breakpointId = bp.StackId, + locations = new [] { + bp_loc.AsLocation () + }, + }; - breakpoints.Add (bp); - - var ok = JObject.FromObject (new { - breakpointId = $"dotnet:{bp.LocalId}", - locations = locations, - }); - - SendResponse (msg_id, Result.Ok (ok), token); + SendResponse (msg_id, Result.OkFromObject (ok), token); } bool GetPossibleBreakpoints (MessageId msg_id, SourceLocation start, SourceLocation end, CancellationToken token) { - var bps = store.FindPossibleBreakpoints (start, end); + var bps = GetContext (msg_id).Store.FindPossibleBreakpoints (start, end); if (bps == null) return false; - var loc = new List (); - foreach (var b in bps) { - loc.Add (b.ToJObject ()); - } - - var o = JObject.FromObject (new { - locations = loc - }); - - SendResponse (msg_id, Result.Ok (o), token); - + SendResponse (msg_id, Result.OkFromObject (new { locations = bps.Select (b => b.AsLocation ()) }), token); return true; } void OnCompileDotnetScript (MessageId msg_id, CancellationToken token) { - var o = JObject.FromObject (new { }); - - SendResponse (msg_id, Result.Ok (o), token); + SendResponse (msg_id, Result.OkFromObject (new { }), token); } - async Task OnGetScriptSource (MessageId msg_id, string script_id, CancellationToken token) + async Task OnGetScriptSource (MessageId msg_id, string script_id, CancellationToken token) { - var id = new SourceId (script_id); - var src_file = store.GetFileById (id); + if (!SourceId.TryParse (script_id, out var id)) + return false; + var src_file = GetContext (msg_id).Store.GetFileById (id); var res = new StringWriter (); - //res.WriteLine ($"//{id}"); try { var uri = new Uri (src_file.Url); + string source = $"// Unable to find document {src_file.SourceUri}"; + if (uri.IsFile && File.Exists(uri.LocalPath)) { using (var f = new StreamReader (File.Open (uri.LocalPath, FileMode.Open))) { await res.WriteAsync (await f.ReadToEndAsync ()); } - var o = JObject.FromObject (new { - scriptSource = res.ToString () - }); - - SendResponse (msg_id, Result.Ok (o), token); + source = res.ToString (); } else if (src_file.SourceUri.IsFile && File.Exists(src_file.SourceUri.LocalPath)) { using (var f = new StreamReader (File.Open (src_file.SourceUri.LocalPath, FileMode.Open))) { await res.WriteAsync (await f.ReadToEndAsync ()); } - var o = JObject.FromObject (new { - scriptSource = res.ToString () - }); - - SendResponse (msg_id, Result.Ok (o), token); + source = res.ToString (); } else if(src_file.SourceLinkUri != null) { var doc = await new WebClient ().DownloadStringTaskAsync (src_file.SourceLinkUri); await res.WriteAsync (doc); - var o = JObject.FromObject (new { - scriptSource = res.ToString () - }); - - SendResponse (msg_id, Result.Ok (o), token); - } else { - var o = JObject.FromObject (new { - scriptSource = $"// Unable to find document {src_file.SourceUri}" - }); - - SendResponse (msg_id, Result.Ok (o), token); + source = res.ToString (); } + + SendResponse (msg_id, Result.OkFromObject (new { scriptSource = source }), token); } catch (Exception e) { - var o = JObject.FromObject (new { + var o = new { scriptSource = $"// Unable to read document ({e.Message})\n" + $"Local path: {src_file?.SourceUri}\n" + $"SourceLink path: {src_file?.SourceLinkUri}\n" - }); + }; - SendResponse (msg_id, Result.Ok (o), token); + SendResponse (msg_id, Result.OkFromObject (o), token); } + return true; } } -} \ No newline at end of file +}