diff options
author | Larry Ewing <lewing@microsoft.com> | 2020-03-05 03:39:15 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-05 03:39:15 +0300 |
commit | a497dc7c5d90e3d458dd8c282ad9f2d4ab510bd0 (patch) | |
tree | 8557da47b4ab895c1ae38f6bf4ce37fa979ce2a2 /sdks | |
parent | a5f8905afa4dfaf71095a590c298acb10d5c2662 (diff) |
[wasm][debugger] Rework breakpointRequest handling across sessions (#19113)
Try to follow CDP ordering constraints by not responding Debugger.enable until
we've fired scriptParsed events for any already loaded assemblies.
Send Debugger.scriptParsed asynchronously as we load assemblies instead of
waiting for them all to load first and fix some of the sequence point processing
to speed up debugger initialization time.
Rework breakpoint request handling by always passing the request to the browser
then storing both the request and the resultant id for use in when any new scripts
are loaded.
Pass any previous breakpoint requests to the new execution context on page reload.
Diffstat (limited to 'sdks')
-rw-r--r-- | sdks/wasm/DebuggerTestSuite/Tests.cs | 19 | ||||
-rw-r--r-- | sdks/wasm/Mono.WebAssembly.DebuggerProxy/DebugStore.cs | 171 | ||||
-rw-r--r-- | sdks/wasm/Mono.WebAssembly.DebuggerProxy/DevToolsProxy.cs | 3 | ||||
-rw-r--r-- | sdks/wasm/Mono.WebAssembly.DebuggerProxy/MonoProxy.cs | 247 |
4 files changed, 242 insertions, 198 deletions
diff --git a/sdks/wasm/DebuggerTestSuite/Tests.cs b/sdks/wasm/DebuggerTestSuite/Tests.cs index a5755a9a9c9..28cc047c84e 100644 --- a/sdks/wasm/DebuggerTestSuite/Tests.cs +++ b/sdks/wasm/DebuggerTestSuite/Tests.cs @@ -77,7 +77,7 @@ namespace DebuggerTests var bp1_res = await SetBreakpoint ("dotnet://debugger-test.dll/debugger-test.cs", 5, 2, ctx); - Assert.Equal ("dotnet:0", bp1_res.Value ["breakpointId"]); + Assert.EndsWith ("debugger-test.cs", bp1_res.Value ["breakpointId"].ToString()); Assert.Equal (1, bp1_res.Value ["locations"]?.Value<JArray> ()?.Count); var loc = bp1_res.Value ["locations"]?.Value<JArray> ()[0]; @@ -123,7 +123,7 @@ namespace DebuggerTests await insp.Ready (async (cli, token) => { ctx = new DebugTestContext (cli, insp, token, scripts); - await SetBreakpoint ("dotnet://debugger-test.dll/debugger-test.cs", 5, 2, ctx); + var bp = await SetBreakpoint ("dotnet://debugger-test.dll/debugger-test.cs", 5, 2, ctx); var eval_req = JObject.FromObject(new { expression = "window.setTimeout(function() { invoke_add(); }, 1);", @@ -135,7 +135,7 @@ namespace DebuggerTests "IntAdd", ctx, wait_for_event_fn: (pause_location) => { Assert.Equal ("other", pause_location ["reason"]?.Value<string> ()); - Assert.Equal ("dotnet:0", pause_location ["hitBreakpoints"]?[0]?.Value<string> ()); + Assert.Equal (bp.Value["breakpointId"]?.ToString(), pause_location ["hitBreakpoints"]?[0]?.Value<string> ()); var top_frame = pause_location ["callFrames"][0]; Assert.Equal ("IntAdd", top_frame ["functionName"].Value<string>()); @@ -347,14 +347,15 @@ namespace DebuggerTests await insp.Ready (async (cli, token) => { var ctx = new DebugTestContext (cli, insp, token, scripts); - await SetBreakpoint (url_key, line, column, ctx); + var bp = await SetBreakpoint (url_key, line, column, ctx); await EvaluateAndCheck ( eval_expression, url_key, line, column, function_name, ctx, wait_for_event_fn: (pause_location) => { //make sure we're on the right bp - Assert.Equal ("dotnet:0", pause_location ["hitBreakpoints"]?[0]?.Value<string> ()); + + Assert.Equal (bp.Value ["breakpointId"]?.ToString (), pause_location ["hitBreakpoints"]?[0]?.Value<string> ()); var top_frame = pause_location ["callFrames"][0]; @@ -380,7 +381,7 @@ namespace DebuggerTests await insp.Ready (async (cli, token) => { var ctx = new DebugTestContext (cli, insp, token, scripts); - await SetBreakpoint ("dotnet://debugger-test.dll/debugger-test.cs", 41, 2, ctx); + var bp = await SetBreakpoint ("dotnet://debugger-test.dll/debugger-test.cs", 41, 2, ctx); await EvaluateAndCheck ( "window.setTimeout(function() { invoke_delegates_test (); }, 1);", @@ -388,7 +389,7 @@ namespace DebuggerTests "IntAdd", ctx, wait_for_event_fn: async (pause_location) => { //make sure we're on the right bp - Assert.Equal ("dotnet:0", pause_location ["hitBreakpoints"]?[0]?.Value<string> ()); + Assert.Equal (bp.Value ["breakpointId"]?.ToString (), pause_location ["hitBreakpoints"]?[0]?.Value<string> ()); var top_frame = pause_location ["callFrames"][0]; @@ -417,7 +418,7 @@ namespace DebuggerTests await insp.Ready (async (cli, token) => { ctx = new DebugTestContext (cli, insp, token, scripts); - await SetBreakpoint ("dotnet://debugger-test.dll/debugger-test.cs", 5, 2, ctx); + var bp = await SetBreakpoint ("dotnet://debugger-test.dll/debugger-test.cs", 5, 2, ctx); await EvaluateAndCheck ( "window.setTimeout(function() { invoke_add(); }, 1);", @@ -425,7 +426,7 @@ namespace DebuggerTests "IntAdd", ctx, wait_for_event_fn: (pause_location) => { //make sure we're on the right bp - Assert.Equal ("dotnet:0", pause_location ["hitBreakpoints"]?[0]?.Value<string> ()); + Assert.Equal (bp.Value ["breakpointId"]?.ToString (), pause_location ["hitBreakpoints"]?[0]?.Value<string> ()); var top_frame = pause_location ["callFrames"][0]; CheckLocation ("dotnet://debugger-test.dll/debugger-test.cs", 3, 41, scripts, top_frame["functionLocation"]); diff --git a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DebugStore.cs b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DebugStore.cs index 8a653ce130a..c5bc9fdcc61 100644 --- a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DebugStore.cs +++ b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DebugStore.cs @@ -12,56 +12,76 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Threading; using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; namespace WebAssembly.Net.Debugging { internal class BreakpointRequest { + public string Id { get; private set; } public string Assembly { get; private set; } public string File { get; private set; } public int Line { get; private set; } public int Column { get; private set; } + JObject request; + + public bool IsResolved => Assembly != null; + public List<Breakpoint> Locations { get; } = new List<Breakpoint> (); + public override string ToString () { return $"BreakpointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}"; } - public static BreakpointRequest Parse (JObject args, DebugStore store) + public object AsSetBreakpointByUrlResponse () + => new { breakpointId = Id, locations = Locations.Select(l => l.Location.AsLocation ()) }; + + public static BreakpointRequest Parse (string id, JObject args) { - // 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 - if (args == null || store == null) - return null; + var breakRequest = new BreakpointRequest () { + Id = id, + request = args + }; + return breakRequest; + } - var url = args? ["url"]?.Value<string> (); - if (url == null) { - var urlRegex = args?["urlRegex"].Value<string>(); - var sourceFile = store?.GetFileByUrlRegex (urlRegex); + public BreakpointRequest Clone () + => new BreakpointRequest { Id = Id, request = request }; - url = sourceFile?.DotNetUrl; + public bool IsMatch (SourceFile sourceFile) + { + var url = request? ["url"]?.Value<string> (); + if (url == null) { + var urlRegex = request?["urlRegex"].Value<string>(); + var regex = new Regex (urlRegex); + return regex.IsMatch (sourceFile.Url.ToString ()) || regex.IsMatch (sourceFile.DocUrl); } - if (url != null && !url.StartsWith ("dotnet://", StringComparison.Ordinal)) { - var sourceFile = store.GetFileByUrl (url); - url = sourceFile?.DotNetUrl; - } + return sourceFile.Url.ToString () == url || sourceFile.DotNetUrl == url; + } - if (url == null) - return null; + public bool TryResolve (SourceFile sourceFile) + { + if (!IsMatch (sourceFile)) + return false; - var parts = ParseDocumentUrl (url); - if (parts.Assembly == null) - return null; + var line = request? ["lineNumber"]?.Value<int> (); + var column = request? ["columnNumber"]?.Value<int> (); - var line = args? ["lineNumber"]?.Value<int> (); - var column = args? ["columnNumber"]?.Value<int> (); if (line == null || column == null) - return null; + return false; - return new BreakpointRequest () { - Assembly = parts.Assembly, - File = parts.DocumentPath, - Line = line.Value, - Column = column.Value - }; + Assembly = sourceFile.AssemblyName; + File = sourceFile.DebuggerFileName; + Line = line.Value; + Column = column.Value; + return true; + } + + public bool TryResolve (DebugStore store) + { + if (request == null || store == null) + return false; + + return store.AllSources().FirstOrDefault (source => TryResolve (source)) != null; } static (string Assembly, string DocumentPath) ParseDocumentUrl (string url) @@ -99,7 +119,6 @@ namespace WebAssembly.Net.Debugging { } } - internal class CliLocation { public CliLocation (MethodInfo method, int offset) { @@ -111,7 +130,6 @@ namespace WebAssembly.Net.Debugging { public int Offset { get; private set; } } - internal class SourceLocation { SourceId id; int line; @@ -268,11 +286,26 @@ namespace WebAssembly.Net.Debugging { this.source = source; var sps = methodDef.DebugInformation.SequencePoints; - if (sps != null && sps.Count > 0) { - StartLocation = new SourceLocation (this, sps [0]); - EndLocation = new SourceLocation (this, sps [sps.Count - 1]); + if (sps == null || sps.Count() < 1) + return; + + SequencePoint start = sps [0]; + SequencePoint end = sps [0]; + + foreach (var sp in sps) { + if (sp.StartLine < start.StartLine) + start = sp; + else if (sp.StartLine == start.StartLine && sp.StartColumn < start.StartColumn) + start = sp; + + if (sp.EndLine > end.EndLine) + end = sp; + else if (sp.EndLine == end.EndLine && sp.EndColumn > end.EndColumn) + end = sp; } + StartLocation = new SourceLocation (this, start); + EndLocation = new SourceLocation (this, end); } public SourceLocation GetLocationByIl (int pos) @@ -556,7 +589,7 @@ namespace WebAssembly.Net.Debugging { public Task<byte[][]> Data { get; set; } } - public async Task Load (SessionId sessionId, string [] loaded_files, CancellationToken token) + public async IAsyncEnumerable<SourceFile> Load (SessionId sessionId, string [] loaded_files, [EnumeratorCancellation] CancellationToken token) { static bool MatchPdb (string asm, string pdb) => Path.ChangeExtension (asm, "pdb") == pdb; @@ -585,12 +618,19 @@ namespace WebAssembly.Net.Debugging { } foreach (var step in steps) { + AssemblyInfo assembly = null; try { var bytes = await step.Data; - assemblies.Add (new AssemblyInfo (step.Url, bytes[0], bytes[1])); + assembly = new AssemblyInfo (step.Url, bytes [0], bytes [1]); } catch (Exception e) { logger.LogDebug ($"Failed to Load {step.Url} ({e.Message})"); } + if (assembly == null) + continue; + + assemblies.Add (assembly); + foreach (var source in assembly.Sources) + yield return source; } } @@ -598,7 +638,7 @@ namespace WebAssembly.Net.Debugging { => assemblies.SelectMany (a => a.Sources); public SourceFile GetFileById (SourceId id) - => AllSources ().FirstOrDefault (f => f.SourceId.Equals (id)); + => AllSources ().SingleOrDefault (f => f.SourceId.Equals (id)); public AssemblyInfo GetAssemblyByName (string name) => assemblies.FirstOrDefault (a => a.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase)); @@ -612,15 +652,16 @@ namespace WebAssembly.Net.Debugging { var spStart = (Line: sp.StartLine - 1, Column: sp.StartColumn - 1); var spEnd = (Line: sp.EndLine - 1, Column: sp.EndColumn - 1); - if (start.Line > spStart.Line) + if (start.Line > spEnd.Line) return false; - if (start.Column > spStart.Column && start.Line == sp.StartLine) + + if (start.Column > spEnd.Column && start.Line == spEnd.Line) return false; - if (end.Line < spEnd.Line) + if (end.Line < spStart.Line) return false; - if (end.Column < spEnd.Column && end.Line == spEnd.Line) + if (end.Column < spStart.Column && end.Line == spStart.Line) return false; return true; @@ -629,22 +670,25 @@ namespace WebAssembly.Net.Debugging { public List<SourceLocation> FindPossibleBreakpoints (SourceLocation start, SourceLocation end) { //XXX FIXME no idea what todo with locations on different files - if (start.Id != end.Id) + if (start.Id != end.Id) { + logger.LogDebug ($"FindPossibleBreakpoints: documents differ (start: {start.Id}) (end {end.Id}"); return null; - var src_id = start.Id; + } - var doc = GetFileById (src_id); + var sourceId = start.Id; + + var doc = GetFileById (sourceId); var res = new List<SourceLocation> (); if (doc == null) { - logger.LogDebug ($"Could not find document {src_id}"); + logger.LogDebug ($"Could not find document {sourceId}"); return res; } - foreach (var m in doc.Methods) { - foreach (var sp in m.methodDef.DebugInformation.SequencePoints) { - if (Match (sp, start, end)) - res.Add (new SourceLocation (m, sp)); + foreach (var method in doc.Methods) { + foreach (var sequencePoint in method.methodDef.DebugInformation.SequencePoints) { + if (!sequencePoint.IsHidden && Match (sequencePoint, start, end)) + res.Add (new SourceLocation (method, sequencePoint)); } } return res; @@ -674,35 +718,26 @@ namespace WebAssembly.Net.Debugging { return true; } - public SourceLocation FindBestBreakpoint (BreakpointRequest req) + public IEnumerable<SourceLocation> FindBreakpointLocations (BreakpointRequest request) { - 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)); + request.TryResolve (this); - if (src == null) - return null; + var asm = assemblies.FirstOrDefault (a => a.Name.Equals (request.Assembly, StringComparison.OrdinalIgnoreCase)); + var sourceFile = asm?.Sources?.SingleOrDefault (s => s.DebuggerFileName.Equals (request.File, StringComparison.OrdinalIgnoreCase)); + + if (sourceFile == null) + yield break; - foreach (var m in src.Methods) { - foreach (var sp in m.methodDef.DebugInformation.SequencePoints) { + foreach (var method in sourceFile.Methods) { + foreach (var sequencePoint in method.methodDef.DebugInformation.SequencePoints) { //FIXME handle multi doc methods - if (Match (sp, req.Line, req.Column)) - return new SourceLocation (m, sp); + if (!sequencePoint.IsHidden && Match (sequencePoint, request.Line, request.Column)) + yield return new SourceLocation (method, sequencePoint); } } - - return null; } public string ToUrl (SourceLocation location) => location != null ? GetFileById (location.Id).Url : ""; - - public SourceFile GetFileByUrlRegex (string urlRegex) - { - var regex = new Regex (urlRegex); - return AllSources ().FirstOrDefault (file => regex.IsMatch (file.Url.ToString()) || regex.IsMatch (file.DocUrl)); - } - - public SourceFile GetFileByUrl (string url) - => AllSources ().FirstOrDefault (file => file.Url.ToString() == url); } } diff --git a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DevToolsProxy.cs b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DevToolsProxy.cs index 158fdcc1dfb..e6aee451de1 100644 --- a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DevToolsProxy.cs +++ b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/DevToolsProxy.cs @@ -290,13 +290,14 @@ namespace WebAssembly.Net.Debugging { internal void SendResponse (MessageId id, Result result, CancellationToken token) { - //Log ("verbose", $"sending response: {id}: {result.ToJObject (id)}"); SendResponseInternal (id, result, token); } void SendResponseInternal (MessageId id, Result result, CancellationToken token) { JObject o = result.ToJObject (id); + if (result.IsErr) + logger.LogError ("sending error response {result}", result); Send (this.ide, o, token); } diff --git a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/MonoProxy.cs b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/MonoProxy.cs index 8403d612a7f..b5dd1be314a 100644 --- a/sdks/wasm/Mono.WebAssembly.DebuggerProxy/MonoProxy.cs +++ b/sdks/wasm/Mono.WebAssembly.DebuggerProxy/MonoProxy.cs @@ -75,10 +75,9 @@ namespace WebAssembly.Net.Debugging { 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 string StackId => $"dotnet:{LocalId}"; + public string StackId { get; private set; } public static bool TryParseId (string stackId, out int id) { @@ -89,10 +88,10 @@ namespace WebAssembly.Net.Debugging { return int.TryParse (stackId.Substring ("dotnet:".Length), out id); } - public Breakpoint (SourceLocation loc, int localId, BreakpointState state) + public Breakpoint (string stackId, SourceLocation loc, BreakpointState state) { + this.StackId = stackId; this.Location = loc; - this.LocalId = localId; this.State = state; } } @@ -110,18 +109,17 @@ namespace WebAssembly.Net.Debugging { } internal class ExecutionContext { - int breakpointIndex = -1; - public List<Breakpoint> Breakpoints { get; } = new List<Breakpoint> (); + public string DebuggerId { get; set; } + public Dictionary<string,BreakpointRequest> BreakpointRequests { get; } = new Dictionary<string,BreakpointRequest> (); + + public TaskCompletionSource<DebugStore> ready = null; + public bool IsRuntimeReady => ready != null && ready.Task.IsCompleted; - 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> (); @@ -149,6 +147,14 @@ namespace WebAssembly.Net.Debugging { throw new ArgumentException ($"Invalid Session: \"{id}\"", nameof (sessionId)); } + bool UpdateContext (SessionId sessionId, ExecutionContext executionContext, out ExecutionContext previousExecutionContext) + { + var id = sessionId?.sessionId ?? "default"; + var previous = contexts.TryGetValue (id, out previousExecutionContext); + contexts[id] = executionContext; + return previous; + } + internal Task<Result> SendMonoCommand (SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand (id, "Runtime.evaluate", JObject.FromObject (cmd), token); @@ -163,6 +169,7 @@ namespace WebAssembly.Net.Debugging { } break; } + case "Runtime.executionContextCreated": { SendEvent (sessionId, method, args, token); var ctx = args? ["context"]; @@ -187,27 +194,47 @@ namespace WebAssembly.Net.Debugging { break; } + case "Debugger.breakpointResolved": { + break; + } + case "Debugger.scriptParsed":{ 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}"); + Log ("verbose", $"ignoring wasm: Debugger.scriptParsed {url}"); return true; } } - Log ("info", $"proxying Debugger.scriptParsed ({sessionId.sessionId}) {url} {args}"); + Log ("verbose", $"proxying Debugger.scriptParsed ({sessionId.sessionId}) {url} {args}"); break; } } return false; } + async Task<bool> IsRuntimeAlreadyReadyAlready (SessionId sessionId, CancellationToken token) + { + var res = await SendMonoCommand (sessionId, MonoCommands.IsRuntimeReady (), token); + return res.Value? ["result"]? ["value"]?.Value<bool> () ?? false; + } protected override async Task<bool> AcceptCommand (MessageId id, string method, JObject args, CancellationToken token) { switch (method) { + case "Debugger.enable": { + var resp = await SendCommand (id, method, args, token); + + GetContext (id).DebuggerId = resp.Value ["debuggerId"]?.ToString (); + + if (await IsRuntimeAlreadyReadyAlready (id, token)) + await RuntimeReady (id, token); + + SendResponse (id,resp,token); + return true; + } case "Debugger.getScriptSource": { var script = args? ["scriptId"]?.Value<string> (); @@ -240,19 +267,27 @@ namespace WebAssembly.Net.Debugging { return true; } + case "Debugger.setBreakpoint": { + break; + } + case "Debugger.setBreakpointByUrl": { + var context = GetContext (id); var resp = await SendCommand (id, method, args, token); - if (resp.IsOk && resp.Value ["locations"].HasValues) { + if (!resp.IsOk) { SendResponse (id, resp, token); return true; } - 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; + var bpid = resp.Value["breakpointId"]?.ToString (); + var request = BreakpointRequest.Parse (bpid, args); + context.BreakpointRequests[bpid] = request; + var store = await RuntimeReady (id, token); - SendResponse (id, resp, token); + Log ("verbose", $"BP req {args}"); + await SetBreakpoint (id, store, request, token); + + SendResponse (id, Result.OkFromObject (request.AsSetBreakpointByUrlResponse()), token); return true; } @@ -333,10 +368,8 @@ namespace WebAssembly.Net.Debugging { //Give up and send the original call stack return false; } - 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 bp = context.BreakpointRequests.Values.SelectMany (v => v.Locations).FirstOrDefault (b => b.RemoteId == bp_id.Value); var callFrames = new List<object> (); foreach (var frame in orig_callframes) { @@ -352,6 +385,7 @@ namespace WebAssembly.Net.Debugging { var method_token = mono_frame ["method_token"].Value<int> (); var assembly_name = mono_frame ["assembly_name"].Value<string> (); + var store = await LoadStore (sessionId, token); var asm = store.GetAssemblyByName (assembly_name); if (asm == null) { Log ("info",$"Unable to find assembly: {assembly_name}"); @@ -430,25 +464,19 @@ namespace WebAssembly.Net.Debugging { 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 context.Breakpoints){ - b.State = BreakpointState.Pending; + if (UpdateContext (sessionId, context, out var previousContext)) { + foreach (var kvp in previousContext.BreakpointRequests) { + context.BreakpointRequests[kvp.Key] = kvp.Value.Clone(); + } } - Log ("debug", "checking if the runtime is ready"); - var res = await SendMonoCommand (sessionId, MonoCommands.IsRuntimeReady (), token); - var is_ready = res.Value? ["result"]? ["value"]?.Value<bool> (); - - if (is_ready.HasValue && is_ready.Value == true) { + if (await IsRuntimeAlreadyReadyAlready (sessionId, token)) await RuntimeReady (sessionId, token); - } } async Task OnResume (MessageId msd_id, CancellationToken token) { - //discard frames + //discard managed frames GetContext (msd_id).CallStack = null; await Task.CompletedTask; } @@ -487,8 +515,7 @@ namespace WebAssembly.Net.Debugging { 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) - { + if (res.IsErr) { SendResponse(msg_id, res, token); return; } @@ -544,8 +571,10 @@ namespace WebAssembly.Net.Debugging { var values = res.Value? ["result"]? ["value"]?.Values<JObject> ().ToArray (); - if(values == null) + if(values == null) { SendResponse (msg_id, Result.OkFromObject (new {result = Array.Empty<object> ()}), token); + return; + } var var_list = new List<object> (); int i = 0; @@ -589,8 +618,9 @@ namespace WebAssembly.Net.Debugging { } } - async Task<Result> EnableBreakpoint (SessionId sessionId, Breakpoint bp, CancellationToken token) + async Task<Breakpoint> SetMonoBreakpoint (SessionId sessionId, BreakpointRequest req, SourceLocation location, CancellationToken token) { + var bp = new Breakpoint (req.Id, location, BreakpointState.Pending); var asm_name = bp.Location.CliLocation.Method.Assembly.Name; var method_token = bp.Location.CliLocation.Method.Token; var il_offset = bp.Location.CliLocation.Offset; @@ -604,147 +634,124 @@ namespace WebAssembly.Net.Debugging { //Log ("verbose", $"BP local id {bp.LocalId} enabled with remote id {bp.RemoteId}"); } - return res; + return bp; } async Task<DebugStore> LoadStore (SessionId sessionId, CancellationToken token) { var context = GetContext (sessionId); - if (Interlocked.CompareExchange (ref context.store, new DebugStore (logger), null) != null) { + if (Interlocked.CompareExchange (ref context.store, new DebugStore (logger), null) != null) return await context.Source.Task; - } 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); + await foreach (var source in context.store.Load(sessionId, the_pdbs, token).WithCancellation (token)) { + var scriptSource = JObject.FromObject (source.ToScriptSource (context.Id, context.AuxData)); + Log ("verbose", $"\tsending {source.Url} {context.Id} {sessionId.sessionId}"); + + SendEvent (sessionId, "Debugger.scriptParsed", scriptSource, token); + + foreach (var req in context.BreakpointRequests.Values) { + if (req.TryResolve (source)) { + await SetBreakpoint (sessionId, context.store, req, token); + } + } + } } catch (Exception e) { context.Source.SetException (e); } if (!context.Source.Task.IsCompleted) context.Source.SetResult (context.store); - return await context.Source.Task; + return context.store; } - async Task RuntimeReady (SessionId sessionId, CancellationToken token) + async Task<DebugStore> RuntimeReady (SessionId sessionId, CancellationToken token) { var context = GetContext (sessionId); - if (context.RuntimeReady) - return; + if (Interlocked.CompareExchange (ref context.ready, new TaskCompletionSource<DebugStore> (), null) != null) + return await context.ready.Task; 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); - 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 context.Breakpoints) { - if (bp.State != BreakpointState.Pending) - continue; - var res = await EnableBreakpoint (sessionId, bp, token); - var ret_code = res.Value? ["result"]? ["value"]?.Value<int> (); - - //if we fail we just buble that to the IDE (and let it panic over it) - 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; - } - } + context.ready.SetResult (store); SendEvent (sessionId, "Mono.runtimeReady", new JObject (), token); + return store; } async Task<bool> RemoveBreakpoint(MessageId msg_id, JObject args, CancellationToken token) { var bpid = args? ["breakpointId"]?.Value<string> (); - if (!Breakpoint.TryParseId (bpid, out var the_id)) - return false; - 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}"); + if (!context.BreakpointRequests.TryGetValue (bpid, out var breakpointRequest)) return false; - } - - context.Breakpoints.Remove (bp); - //FIXME verify result (and log?) - var res = await RemoveBreakpoint (msg_id, bp, token); - - return true; - } - - 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> (); + foreach (var bp in breakpointRequest.Locations) { + var res = await SendMonoCommand (msg_id, 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; + if (ret_code.HasValue) { + bp.RemoteId = -1; + bp.State = BreakpointState.Disabled; + } } - - return res; + breakpointRequest.Locations.Clear (); + return false; } - async Task<bool> SetBreakpoint (MessageId msg_id, BreakpointRequest req, CancellationToken token) + async Task SetBreakpoint (SessionId sessionId, DebugStore store, BreakpointRequest req, CancellationToken token) { - 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 ("verbose", $"Could not resolve breakpoint request: {req}"); - return false; + var context = GetContext (sessionId); + if (req.Locations.Any ()) { + Log ("debug", $"locations already loaded for {req.Id}"); + return; } - Breakpoint bp = null; - if (!context.RuntimeReady) { - bp = new Breakpoint (bp_loc, context.NextBreakpointId (), BreakpointState.Pending); - } else { - bp = new Breakpoint (bp_loc, context.NextBreakpointId (), BreakpointState.Disabled); + var locations = store.FindBreakpointLocations (req).ToList (); + logger.LogDebug ("BP request for '{req}' runtime ready {context.RuntimeReady}", req, GetContext (sessionId).IsRuntimeReady); - var res = await EnableBreakpoint (msg_id, bp, token); - var ret_code = res.Value? ["result"]? ["value"]?.Value<int> (); + var breakpoints = new List<Breakpoint> (); + foreach (var loc in locations) { + var bp = await SetMonoBreakpoint (sessionId, req, loc, token); - //if we fail we just buble that to the IDE (and let it panic over it) - if (!ret_code.HasValue) { - return false; - } - } + // If we didn't successfully enable the breakpoint + // don't add it to the list of locations for this id + if (bp.State != BreakpointState.Active) + continue; + + breakpoints.Add (bp); - context.Breakpoints.Add (bp); + var resolvedLocation = new { + breakpointId = req.Id, + location = loc.AsLocation () + }; - var ok = new { - breakpointId = bp.StackId, - locations = new [] { - bp_loc.AsLocation () - }, - }; + SendEvent (sessionId, "Debugger.breakpointResolved", JObject.FromObject (resolvedLocation), token); + } - SendResponse (msg_id, Result.OkFromObject (ok), token); - return true; + req.Locations.AddRange (breakpoints); + return; } - async Task<bool> GetPossibleBreakpoints (MessageId msg_id, SourceLocation start, SourceLocation end, CancellationToken token) + async Task<bool> GetPossibleBreakpoints (MessageId msg, SourceLocation start, SourceLocation end, CancellationToken token) { - var bps = (await LoadStore (msg_id, token)).FindPossibleBreakpoints (start, end); + var bps = (await RuntimeReady (msg, token)).FindPossibleBreakpoints (start, end); + if (bps == null) return false; - SendResponse (msg_id, Result.OkFromObject (new { locations = bps.Select (b => b.AsLocation ()) }), token); + var response = new { locations = bps.Select (b => b.AsLocation ()) }; + + SendResponse (msg, Result.OkFromObject (response), token); return true; } |