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 8a653ce130..c5bc9fdcc6 100644 --- a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs +++ b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/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 Locations { get; } = new List (); + 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 url = args? ["url"]?.Value (); - if (url == null) { - var urlRegex = args?["urlRegex"].Value(); - var sourceFile = store?.GetFileByUrlRegex (urlRegex); - - url = sourceFile?.DotNetUrl; - } - - if (url != null && !url.StartsWith ("dotnet://", StringComparison.Ordinal)) { - var sourceFile = store.GetFileByUrl (url); - url = sourceFile?.DotNetUrl; - } - - if (url == null) - return null; - - var parts = ParseDocumentUrl (url); - if (parts.Assembly == null) - return null; - - var line = args? ["lineNumber"]?.Value (); - var column = args? ["columnNumber"]?.Value (); - if (line == null || column == null) - return null; - - return new BreakpointRequest () { - Assembly = parts.Assembly, - File = parts.DocumentPath, - Line = line.Value, - Column = column.Value + var breakRequest = new BreakpointRequest () { + Id = id, + request = args }; + return breakRequest; + } + + public BreakpointRequest Clone () + => new BreakpointRequest { Id = Id, request = request }; + + public bool IsMatch (SourceFile sourceFile) + { + var url = request? ["url"]?.Value (); + if (url == null) { + var urlRegex = request?["urlRegex"].Value(); + var regex = new Regex (urlRegex); + return regex.IsMatch (sourceFile.Url.ToString ()) || regex.IsMatch (sourceFile.DocUrl); + } + + return sourceFile.Url.ToString () == url || sourceFile.DotNetUrl == url; + } + + public bool TryResolve (SourceFile sourceFile) + { + if (!IsMatch (sourceFile)) + return false; + + var line = request? ["lineNumber"]?.Value (); + var column = request? ["columnNumber"]?.Value (); + + if (line == null || column == null) + return false; + + 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 Data { get; set; } } - public async Task Load (SessionId sessionId, string [] loaded_files, CancellationToken token) + public async IAsyncEnumerable 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) - return false; - if (start.Column > spStart.Column && start.Line == sp.StartLine) + if (start.Line > spEnd.Line) return false; - if (end.Line < spEnd.Line) + if (start.Column > spEnd.Column && start.Line == spEnd.Line) return false; - if (end.Column < spEnd.Column && end.Line == spEnd.Line) + if (end.Line < spStart.Line) + return false; + + if (end.Column < spStart.Column && end.Line == spStart.Line) return false; return true; @@ -629,22 +670,25 @@ namespace WebAssembly.Net.Debugging { public List 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 (); 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 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)); - foreach (var m in src.Methods) { - foreach (var sp in m.methodDef.DebugInformation.SequencePoints) { + if (sourceFile == null) + yield break; + + 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/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs index 158fdcc1df..e6aee451de 100644 --- a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs +++ b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/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/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs index 8403d612a7..b5dd1be314 100644 --- a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs +++ b/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/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 Breakpoints { get; } = new List (); + public string DebuggerId { get; set; } + public Dictionary BreakpointRequests { get; } = new Dictionary (); + + public TaskCompletionSource 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 CallStack { get; set; } - public int NextBreakpointId () - => Interlocked.Increment (ref breakpointIndex); - internal DebugStore store; public TaskCompletionSource Source { get; } = new TaskCompletionSource (); @@ -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 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 () ?? ""; 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 IsRuntimeAlreadyReadyAlready (SessionId sessionId, CancellationToken token) + { + var res = await SendMonoCommand (sessionId, MonoCommands.IsRuntimeReady (), token); + return res.Value? ["result"]? ["value"]?.Value () ?? false; + } protected override async Task 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 (); @@ -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 (); foreach (var frame in orig_callframes) { @@ -352,6 +385,7 @@ namespace WebAssembly.Net.Debugging { var method_token = mono_frame ["method_token"].Value (); var assembly_name = mono_frame ["assembly_name"].Value (); + 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 (); - - 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 ().ToArray (); - if(values == null) + if(values == null) { SendResponse (msg_id, Result.OkFromObject (new {result = Array.Empty ()}), token); + return; + } var var_list = new List (); int i = 0; @@ -589,8 +618,9 @@ namespace WebAssembly.Net.Debugging { } } - async Task EnableBreakpoint (SessionId sessionId, Breakpoint bp, CancellationToken token) + async Task 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 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 (); - 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 RuntimeReady (SessionId sessionId, CancellationToken token) { var context = GetContext (sessionId); - if (context.RuntimeReady) - return; + if (Interlocked.CompareExchange (ref context.ready, new TaskCompletionSource (), 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 (); - - //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 RemoveBreakpoint(MessageId msg_id, JObject args, CancellationToken token) { var bpid = args? ["breakpointId"]?.Value (); - 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 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 (); - - if (ret_code.HasValue) { - bp.RemoteId = -1; - bp.State = BreakpointState.Disabled; - } - - return res; - } - - async Task SetBreakpoint (MessageId msg_id, 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; - } - - 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 res = await EnableBreakpoint (msg_id, bp, token); + 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 (); - //if we fail we just buble that to the IDE (and let it panic over it) - if (!ret_code.HasValue) { - return false; + if (ret_code.HasValue) { + bp.RemoteId = -1; + bp.State = BreakpointState.Disabled; } } - - context.Breakpoints.Add (bp); - - var ok = new { - breakpointId = bp.StackId, - locations = new [] { - bp_loc.AsLocation () - }, - }; - - SendResponse (msg_id, Result.OkFromObject (ok), token); - return true; + breakpointRequest.Locations.Clear (); + return false; } - async Task GetPossibleBreakpoints (MessageId msg_id, SourceLocation start, SourceLocation end, CancellationToken token) + async Task SetBreakpoint (SessionId sessionId, DebugStore store, BreakpointRequest req, CancellationToken token) { - var bps = (await LoadStore (msg_id, token)).FindPossibleBreakpoints (start, end); + var context = GetContext (sessionId); + if (req.Locations.Any ()) { + Log ("debug", $"locations already loaded for {req.Id}"); + return; + } + + var locations = store.FindBreakpointLocations (req).ToList (); + logger.LogDebug ("BP request for '{req}' runtime ready {context.RuntimeReady}", req, GetContext (sessionId).IsRuntimeReady); + + var breakpoints = new List (); + foreach (var loc in locations) { + var bp = await SetMonoBreakpoint (sessionId, req, loc, token); + + // 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); + + var resolvedLocation = new { + breakpointId = req.Id, + location = loc.AsLocation () + }; + + SendEvent (sessionId, "Debugger.breakpointResolved", JObject.FromObject (resolvedLocation), token); + } + + req.Locations.AddRange (breakpoints); + return; + } + + async Task GetPossibleBreakpoints (MessageId msg, SourceLocation start, SourceLocation end, CancellationToken token) + { + 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; }