diff options
author | Larry Ewing <lewing@microsoft.com> | 2020-02-22 00:06:45 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-02-22 00:06:45 +0300 |
commit | dce53cc9f31aab3883ca85303ba42ec9b22caea7 (patch) | |
tree | a914466a889de270a48c8df81116c73dde5dfe93 /sdks | |
parent | 840ba017efe59a531e3ea1ea9c7b31e968a428d3 (diff) |
[wasm][debugger] Store more per session state and force some event ordering (#18747)
* [wasm][debugger] Store more per session state and force some event ordering
Register a DebugStore and ExecutionContext per session and force some commands
to block on source loading so that scripts are loaded before attempting to use
them. This should populate the sources in vscode-js-debug properly.
Diffstat (limited to 'sdks')
4 files changed, 276 insertions, 244 deletions
diff --git a/sdks/wasm/DebuggerTestSuite/Support.cs b/sdks/wasm/DebuggerTestSuite/Support.cs index 8c5a251eaa4..38b844fc289 100644 --- a/sdks/wasm/DebuggerTestSuite/Support.cs +++ b/sdks/wasm/DebuggerTestSuite/Support.cs @@ -112,6 +112,7 @@ namespace DebuggerTests static string[] PROBE_LIST = { "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/usr/bin/chromium", "/usr/bin/chromium-browser", }; diff --git a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DebugStore.cs b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DebugStore.cs index 520b61653ce..3e45adbbd75 100644 --- a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DebugStore.cs +++ b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DebugStore.cs @@ -13,17 +13,17 @@ using System.Threading.Tasks; using System.Threading; 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 +38,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 +55,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, @@ -68,7 +68,7 @@ namespace WebAssembly.Net.Debugging { if (Uri.TryCreate (url, UriKind.Absolute, out var docUri) && docUri.Scheme == "dotnet") { return ( docUri.Host, - docUri.PathAndQuery.Substring (1) + docUri.PathAndQuery ); } else { return (null, null); @@ -147,7 +147,9 @@ namespace WebAssembly.Net.Debugging { if (obj == null) return null; - var id = SourceId.TryParse (obj ["scriptId"]?.Value<string> ()); + if (!SourceId.TryParse (obj ["scriptId"]?.Value<string> (), out var id)) + return null; + var line = obj ["lineNumber"]?.Value<int> (); var column = obj ["columnNumber"]?.Value<int> (); if (id == null || line == null || column == null) @@ -156,18 +158,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 +180,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) @@ -296,9 +299,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; @@ -360,7 +361,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,27 +474,54 @@ 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 string DotNetUrl => $"dotnet://{assembly.Name}{DebuggerFileName}"; + public SourceId SourceId => new SourceId (assembly.Id, this.id); public Uri SourceLinkUri { get; } public Uri SourceUri { get; } public IEnumerable<MethodInfo> 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<byte[]> 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; List<AssemblyInfo> assemblies = new List<AssemblyInfo> (); HttpClient client = new HttpClient (); @@ -527,15 +555,6 @@ namespace WebAssembly.Net.Debugging { }); } 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); - } } @@ -630,7 +649,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/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DevToolsProxy.cs b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DevToolsProxy.cs index 97b8fad5efb..ace484cb372 100644 --- a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DevToolsProxy.cs +++ b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DevToolsProxy.cs @@ -40,6 +40,9 @@ namespace WebAssembly.Net.Debugging { public static Result Ok (JObject ok) => new Result (ok, null); + public static Result OkFromObject (object ok) + => Ok (JObject.FromObject(ok)); + public static Result Err (JObject err) => new Result (null, err); @@ -96,7 +99,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<byte> (pending [0]), WebSocketMessageType.Text, true, token); return current_send; } @@ -234,7 +237,7 @@ namespace WebAssembly.Net.Debugging { Task<Result> 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, @@ -244,7 +247,6 @@ namespace WebAssembly.Net.Debugging { }); var tcs = new TaskCompletionSource<Result> (); - var msgId = new MessageId { id = id, sessionId = sessionId.sessionId }; //Log ("verbose", $"add cmd id {sessionId}-{id}"); pending_cmds.Add ((msgId , tcs)); diff --git a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/MonoProxy.cs b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/MonoProxy.cs index 4aa29eda90c..d2f78e82eec 100644 --- a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/MonoProxy.cs +++ b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/MonoProxy.cs @@ -76,9 +76,19 @@ namespace WebAssembly.Net.Debugging { 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; @@ -86,7 +96,7 @@ namespace WebAssembly.Net.Debugging { } } - enum BreakPointState { + enum BreakpointState { Active, Disabled, Pending @@ -98,17 +108,46 @@ namespace WebAssembly.Net.Debugging { Over } + internal class ExecutionContext { + int breakpointIndex = -1; + public List<Breakpoint> Breakpoints { get; } = new List<Breakpoint> (); + + public bool RuntimeReady { get; set; } + public int Id { get; set; } + public object AuxData { get; set; } + + public List<Frame> CallStack { get; set; } + + public int NextBreakpointId () + => Interlocked.Increment (ref breakpointIndex); + + internal DebugStore store; + public TaskCompletionSource<DebugStore> Source { get; } = new TaskCompletionSource<DebugStore> (); + + public DebugStore Store { + get { + if (store == null || !Source.Task.IsCompleted) + return null; + + return store; + } + } + } + public class MonoProxy : DevToolsProxy { - DebugStore store; - List<Breakpoint> breakpoints = new List<Breakpoint> (); - List<Frame> current_callstack; - bool runtime_ready; - int local_breakpoint_id; - int ctx_id; - JObject aux_ctx_data; + Dictionary<string, ExecutionContext> contexts = new Dictionary<string, ExecutionContext> (); public MonoProxy () { } + 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<Result> SendMonoCommand (SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand (id, "Runtime.evaluate", JObject.FromObject (cmd), token); @@ -116,40 +155,45 @@ namespace WebAssembly.Net.Debugging { { 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<int> (); if (aux_data != null) { var is_default = aux_data ["isDefault"]?.Value<bool> (); if (is_default == true) { - var id = new MessageId { id = ctx ["id"].Value<int> (), 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<string> (); + 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<string> ()?.StartsWith ("wasm://") == true) { - // Console.WriteLine ("ignoring wasm event"); - return true; + var url = args? ["url"]?.Value<string> () ?? ""; + + 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": { - if (store == null) - await LoadStore (new SessionId { sessionId = args? ["sessionId"]?.Value<string> () }, token); + Log ("info", $"proxying Debugger.scriptParsed ({sessionId.sessionId}) {url} {args}"); break; } } @@ -160,24 +204,15 @@ namespace WebAssembly.Net.Debugging { protected override async Task<bool> 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<string> (); - if (script_id.StartsWith ("dotnet://", StringComparison.InvariantCultureIgnoreCase)) { - await OnGetScriptSource (id, script_id, token); - return true; - } - break; + var script = args? ["scriptId"]?.Value<string> (); + return await OnGetScriptSource (id, script, token); } case "Runtime.compileScript": { var exp = args? ["expression"]?.Value<string> (); - if (exp.StartsWith ("//dotnet:", StringComparison.InvariantCultureIgnoreCase)) { + if (exp.StartsWith ("//dotnet:", StringComparison.Ordinal)) { OnCompileDotnetScript (id, token); return true; } @@ -194,8 +229,8 @@ namespace WebAssembly.Net.Debugging { } case "Debugger.setBreakpointByUrl": { - Log ("verbose", $"BP req {args}"); - var bp_req = BreakPointRequest.Parse (args, store); + Log ("info", $"BP req {args}"); + var bp_req = BreakpointRequest.Parse (args, GetContext (id).Store); if (bp_req != null) { await SetBreakPoint (id, bp_req, token); return true; @@ -208,37 +243,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<string> (); - if (objId.StartsWith ("dotnet:")) { + if (objId.StartsWith ("dotnet:", StringComparison.Ordinal)) { var parts = objId.Split (new char [] { ':' }); if (parts.Length < 3) return true; @@ -267,31 +290,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<bool> 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 res = await SendMonoCommand (sessionId, MonoCommands.GetCallStack(), token); var orig_callframes = args? ["callFrames"]?.Values<JObject> (); + 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}"); @@ -299,14 +321,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<JObject> (); + var callFrames = new List<object> (); foreach (var frame in orig_callframes) { var function_name = frame ["functionName"]?.Value<string> (); var url = frame ["url"]?.Value<string> (); @@ -347,12 +369,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), @@ -366,80 +388,85 @@ 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; var o = JObject.FromObject (new { - callFrames = callFrames, + 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; - this.ctx_id = ctx_id.id; - this.aux_ctx_data = aux_data; - Log ("verbose", "checking if the runtime is ready"); - var res = await SendMonoCommand (ctx_id, MonoCommands.IsRuntimeReady (), 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<bool> (); //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<bool> Step (MessageId msg_id, StepKind kind, CancellationToken token) { + var context = GetContext (msg_id); + if (context.CallStack == null) + return false; + 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; } static string FormatFieldName (string name) { - if (name.Contains("k__BackingField")) { - return name.Replace("k__BackingField", "") - .Replace("<", "") - .Replace(">", ""); + if (name.Contains("k__BackingField", StringComparison.Ordinal)) { + return name.Replace("k__BackingField", "", StringComparison.Ordinal) + .Replace("<", "", StringComparison.Ordinal) + .Replace(">", "", StringComparison.Ordinal); } return name; } @@ -490,11 +517,11 @@ namespace WebAssembly.Net.Debugging { async Task GetScopeProperties (MessageId msg_id, int scope_id, CancellationToken token) { + try { - var scope = this.current_callstack.FirstOrDefault (s => s.Id == scope_id); + 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); @@ -506,20 +533,23 @@ namespace WebAssembly.Net.Debugging { var values = res.Value? ["result"]? ["value"]?.Values<JObject> ().ToArray (); - var var_list = new List<JObject> (); + if(values == null) + SendResponse (msg_id, Result.OkFromObject (new {result = Array.Empty<object> ()}), token); + + var var_list = new List<object> (); 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 @@ -534,16 +564,14 @@ 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; } - var 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, Result.Exception (exception), token); @@ -561,39 +589,49 @@ namespace WebAssembly.Net.Debugging { 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<DebugStore> LoadStore (SessionId sessionId, CancellationToken token) { - var loaded_pdbs = await SendMonoCommand (sessionId, MonoCommands.GetLoadedFiles(), token); - var the_value = loaded_pdbs.Value? ["result"]? ["value"]; - var the_pdbs = the_value?.ToObject<string[]> (); + var context = GetContext (sessionId); + + if (Interlocked.CompareExchange (ref context.store, new DebugStore (), 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<string[]> (); + + 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; + + context.RuntimeReady = true; + var store = await LoadStore (sessionId, token); 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 scriptSource = JObject.FromObject (s.ToScriptSource (context.Id, context.AuxData)); + Log ("verbose", $"\tsending {s.Url} {context.Id} {sessionId.sessionId}"); + SendEvent (sessionId, "Debugger.scriptParsed", scriptSource, token); } var clear_result = await SendMonoCommand (sessionId, MonoCommands.ClearAllBreakpoints (), token); @@ -601,10 +639,8 @@ namespace WebAssembly.Net.Debugging { Log ("verbose", $"Failed to clear breakpoints due to {clear_result}"); } - runtime_ready = true; - - 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<int> (); @@ -613,52 +649,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<bool> RemoveBreakpoint(MessageId msg_id, JObject args, CancellationToken token) { var bpid = args? ["breakpointId"]?.Value<string> (); - if (bpid?.StartsWith ("dotnet:") != true) - return false; - var the_id = int.Parse (bpid.Substring ("dotnet:".Length)); + if (!Breakpoint.TryParseId (bpid, out var the_id)) + return false; - 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<Result> RemoveBreakPoint (SessionId sessionId, Breakpoint bp, CancellationToken token) + async Task<Result> RemoveBreakpoint (SessionId sessionId, Breakpoint bp, CancellationToken token) { var res = await SendMonoCommand (sessionId, MonoCommands.RemoveBreakpoint (bp.RemoteId), token); var ret_code = res.Value? ["result"]? ["value"]?.Value<int> (); 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." @@ -667,10 +704,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<int> (); @@ -682,106 +719,79 @@ namespace WebAssembly.Net.Debugging { } } - var locations = new List<JObject> (); - - locations.Add (JObject.FromObject (new { - scriptId = bp_loc.Id.ToString (), - lineNumber = bp_loc.Line, - columnNumber = bp_loc.Column - })); - - breakpoints.Add (bp); + context.Breakpoints.Add (bp); - var ok = JObject.FromObject (new { - breakpointId = $"dotnet:{bp.LocalId}", - locations = locations, - }); + var ok = new { + breakpointId = bp.StackId, + locations = new [] { + new { + scriptId = bp_loc.Id.ToString (), + lineNumber = bp_loc.Line, + columnNumber = bp_loc.Column + } + }, + }; - 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<JObject> (); - 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<bool> 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}" - }); + source = res.ToString (); + } - SendResponse (msg_id, Result.Ok (o), token); - } + 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; } } } |