diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs index a7c75a428e97a3..4ab32b55a5669b 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -407,6 +408,7 @@ public ExecutionContext(MonoSDBHelper sdbAgent, int id, object auxData, PauseOnE AuxData = auxData; SdbAgent = sdbAgent; PauseOnExceptions = pauseOnExceptions; + Destroyed = false; } public string DebugId { get; set; } @@ -440,6 +442,8 @@ public ExecutionContext(MonoSDBHelper sdbAgent, int id, object auxData, PauseOnE internal int TempBreakpointForSetNextIP { get; set; } internal bool FirstBreakpoint { get; set; } + internal bool Destroyed { get; set; } + public DebugStore Store { get @@ -486,4 +490,70 @@ public PerScopeCache() { } } + + internal sealed class ConcurrentExecutionContextDictionary + { + private ConcurrentDictionary> contexts = new (); + public ExecutionContext GetCurrentContext(SessionId sessionId) + => TryGetCurrentExecutionContextValue(sessionId, out ExecutionContext context) + ? context + : throw new KeyNotFoundException($"No execution context found for session {sessionId}"); + + public bool TryGetCurrentExecutionContextValue(SessionId id, out ExecutionContext executionContext, bool ignoreDestroyedContext = true) + { + executionContext = null; + if (!contexts.TryGetValue(id, out ConcurrentBag contextBag)) + return false; + if (contextBag.IsEmpty) + return false; + IEnumerable validContexts = null; + if (ignoreDestroyedContext) + validContexts = contextBag.Where(context => context.Destroyed == false); + else + validContexts = contextBag; + if (!validContexts.Any()) + return false; + int maxId = validContexts.Max(context => context.Id); + executionContext = contextBag.FirstOrDefault(context => context.Id == maxId); + return executionContext != null; + } + + public void OnDefaultContextUpdate(SessionId sessionId, ExecutionContext newContext) + { + if (TryGetAndAddContext(sessionId, newContext, out ExecutionContext previousContext)) + { + foreach (KeyValuePair kvp in previousContext.BreakpointRequests) + { + newContext.BreakpointRequests[kvp.Key] = kvp.Value.Clone(); + } + newContext.PauseOnExceptions = previousContext.PauseOnExceptions; + } + } + + public bool TryGetAndAddContext(SessionId sessionId, ExecutionContext newExecutionContext, out ExecutionContext previousExecutionContext) + { + bool hasExisting = TryGetCurrentExecutionContextValue(sessionId, out previousExecutionContext, ignoreDestroyedContext: false); + ConcurrentBag bag = contexts.GetOrAdd(sessionId, _ => new ConcurrentBag()); + bag.Add(newExecutionContext); + return hasExisting; + } + + public void DestroyContext(SessionId sessionId, int id) + { + if (!contexts.TryGetValue(sessionId, out ConcurrentBag contextBag)) + return; + foreach (ExecutionContext context in contextBag.Where(x => x.Id == id).ToList()) + context.Destroyed = true; + } + + public void ClearContexts(SessionId sessionId) + { + if (!contexts.TryGetValue(sessionId, out ConcurrentBag contextBag)) + return; + foreach (ExecutionContext context in contextBag) + context.Destroyed = true; + } + + public bool ContainsKey(SessionId sessionId) => contexts.ContainsKey(sessionId); + } } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs index deb9761130b3cc..f9e7cf6640e14f 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs @@ -23,7 +23,7 @@ public FirefoxMonoProxy(ILogger logger, string loggerId = null, ProxyOptions opt public FirefoxExecutionContext GetContextFixefox(SessionId sessionId) { - if (contexts.TryGetValue(sessionId, out ExecutionContext context)) + if (Contexts.TryGetCurrentExecutionContextValue(sessionId, out ExecutionContext context)) return context as FirefoxExecutionContext; throw new ArgumentException($"Invalid Session: \"{sessionId}\"", nameof(sessionId)); } @@ -254,7 +254,7 @@ protected override async Task AcceptEvent(SessionId sessionId, JObject arg } if (args["frame"] != null && args["type"] == null) { - OnDefaultContextUpdate(sessionId, new FirefoxExecutionContext(new MonoSDBHelper (this, logger, sessionId), 0, args["frame"]["consoleActor"].Value())); + Contexts.OnDefaultContextUpdate(sessionId, new FirefoxExecutionContext(new MonoSDBHelper (this, logger, sessionId), 0, args["frame"]["consoleActor"].Value())); return false; } @@ -317,7 +317,7 @@ await Task.WhenAll( } case "target-available-form": { - OnDefaultContextUpdate(sessionId, new FirefoxExecutionContext(new MonoSDBHelper (this, logger, sessionId), 0, args["target"]["consoleActor"].Value())); + Contexts.OnDefaultContextUpdate(sessionId, new FirefoxExecutionContext(new MonoSDBHelper (this, logger, sessionId), 0, args["target"]["consoleActor"].Value())); break; } } @@ -334,7 +334,7 @@ protected override async Task AcceptCommand(MessageId sessionId, JObject a { case "resume": { - if (!contexts.TryGetValue(sessionId, out ExecutionContext context)) + if (!Contexts.TryGetCurrentExecutionContextValue(sessionId, out ExecutionContext context)) return false; context.PausedOnWasm = false; if (context.CallStack == null) @@ -396,7 +396,7 @@ protected override async Task AcceptCommand(MessageId sessionId, JObject a } case "setBreakpoint": { - if (!contexts.TryGetValue(sessionId, out ExecutionContext context)) + if (!Contexts.TryGetCurrentExecutionContextValue(sessionId, out ExecutionContext context)) return false; var req = JObject.FromObject(new { @@ -436,7 +436,7 @@ protected override async Task AcceptCommand(MessageId sessionId, JObject a } case "removeBreakpoint": { - if (!contexts.TryGetValue(sessionId, out ExecutionContext context)) + if (!Contexts.TryGetCurrentExecutionContextValue(sessionId, out ExecutionContext context)) return false; Result resp = await SendCommand(sessionId, "", args, token); diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 15682c6dad1e30..781b4c97ff1f2d 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -20,7 +20,7 @@ internal class MonoProxy : DevToolsProxy { private IList urlSymbolServerList; private HashSet sessions = new HashSet(); - protected Dictionary contexts = new Dictionary(); + internal ConcurrentExecutionContextDictionary Contexts = new (); public static HttpClient HttpClient => new HttpClient(); @@ -39,26 +39,11 @@ public MonoProxy(ILogger logger, IList urlSymbolServerList, int runtimeI _defaultPauseOnExceptions = PauseOnExceptionsKind.Unset; } - internal ExecutionContext GetContext(SessionId sessionId) - { - if (contexts.TryGetValue(sessionId, out ExecutionContext context)) - return context; - - throw new ArgumentException($"Invalid Session: \"{sessionId}\"", nameof(sessionId)); - } - - private bool UpdateContext(SessionId sessionId, ExecutionContext executionContext, out ExecutionContext previousExecutionContext) - { - bool previous = contexts.TryGetValue(sessionId, out previousExecutionContext); - contexts[sessionId] = executionContext; - return previous; - } - internal virtual Task SendMonoCommand(SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand(id, "Runtime.evaluate", JObject.FromObject(cmd), token); internal void SendLog(SessionId sessionId, string message, CancellationToken token, string type = "warning") { - if (!contexts.TryGetValue(sessionId, out ExecutionContext context)) + if (!Contexts.TryGetCurrentExecutionContextValue(sessionId, out ExecutionContext context)) return; /*var o = JObject.FromObject(new { @@ -93,7 +78,7 @@ protected override async Task AcceptEvent(SessionId sessionId, JObject par case "Runtime.consoleAPICalled": { // Don't process events from sessions we aren't tracking - if (!contexts.TryGetValue(sessionId, out ExecutionContext context)) + if (!Contexts.TryGetCurrentExecutionContextValue(sessionId, out ExecutionContext context)) return false; string type = args["type"]?.ToString(); if (type == "debug") @@ -169,10 +154,22 @@ protected override async Task AcceptEvent(SessionId sessionId, JObject par return true; } + case "Runtime.executionContextDestroyed": + { + Contexts.DestroyContext(sessionId, args["executionContextId"].Value()); + return false; + } + + case "Runtime.executionContextsCleared": + { + Contexts.ClearContexts(sessionId); + return false; + } + case "Debugger.paused": { // Don't process events from sessions we aren't tracking - if (!contexts.ContainsKey(sessionId)) + if (!Contexts.ContainsKey(sessionId)) return false; if (args?["callFrames"]?.Value()?.Count == 0) @@ -254,7 +251,7 @@ protected virtual async Task SendResume(SessionId id, CancellationToken token) } protected async Task IsRuntimeAlreadyReadyAlready(SessionId sessionId, CancellationToken token) { - if (contexts.TryGetValue(sessionId, out ExecutionContext context) && context.IsRuntimeReady) + if (Contexts.TryGetCurrentExecutionContextValue(sessionId, out ExecutionContext context) && context.IsRuntimeReady) return true; Result res = await SendMonoCommand(sessionId, MonoCommands.IsRuntimeReady(RuntimeId), token); @@ -277,7 +274,7 @@ protected override async Task AcceptCommand(MessageId id, JObject parms, C if (id == SessionId.Null) await AttachToTarget(id, token); - if (!contexts.TryGetValue(id, out ExecutionContext context)) + if (!Contexts.TryGetCurrentExecutionContextValue(id, out ExecutionContext context)) { if (method == "Debugger.setPauseOnExceptions") { @@ -595,7 +592,7 @@ protected override async Task AcceptCommand(MessageId id, JObject parms, C private async Task ApplyUpdates(MessageId id, JObject args, CancellationToken token) { - var context = GetContext(id); + var context = Contexts.GetCurrentContext(id); string moduleGUID = args["moduleGUID"]?.Value(); string dmeta = args["dmeta"]?.Value(); string dil = args["dil"]?.Value(); @@ -664,7 +661,7 @@ internal async Task GetMethodLocation(MessageId id, JObject args, Cancel private async Task CallOnFunction(MessageId id, JObject args, CancellationToken token) { - var context = GetContext(id); + var context = Contexts.GetCurrentContext(id); if (!DotnetObjectId.TryParse(args["objectId"], out DotnetObjectId objectId)) { return false; } @@ -728,7 +725,7 @@ private async Task CallOnFunction(MessageId id, JObject args, Cancellation private async Task OnSetVariableValue(MessageId id, int scopeId, string varName, JToken varValue, CancellationToken token) { - ExecutionContext context = GetContext(id); + ExecutionContext context = Contexts.GetCurrentContext(id); Frame scope = context.CallStack.FirstOrDefault(s => s.Id == scopeId); if (scope == null) return false; @@ -748,7 +745,7 @@ private async Task OnSetVariableValue(MessageId id, int scopeId, string va internal async Task> RuntimeGetObjectMembers(SessionId id, DotnetObjectId objectId, JToken args, CancellationToken token, bool sortByAccessLevel = false) { - var context = GetContext(id); + var context = Contexts.GetCurrentContext(id); GetObjectCommandOptions getObjectOptions = GetObjectCommandOptions.WithProperties; if (args != null) { @@ -1072,7 +1069,7 @@ internal async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObje if (!res.IsOk) return false; - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); byte[] newBytes = Convert.FromBase64String(res.Value?["result"]?["value"]?["value"]?.Value()); using var retDebuggerCmdReader = new MonoBinaryReader(newBytes); retDebuggerCmdReader.ReadBytes(11); //skip HEADER_LEN @@ -1148,7 +1145,7 @@ internal async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObje internal async Task LoadSymbolsOnDemand(AssemblyInfo asm, int method_token, SessionId sessionId, CancellationToken token) { - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); if (urlSymbolServerList.Count == 0) return null; if (asm.TriedToLoadSymbolsOnDemand || !asm.CodeViewInformationAvailable) @@ -1189,29 +1186,17 @@ internal async Task LoadSymbolsOnDemand(AssemblyInfo asm, int method return null; } - protected void OnDefaultContextUpdate(SessionId sessionId, ExecutionContext context) - { - if (UpdateContext(sessionId, context, out ExecutionContext previousContext)) - { - foreach (KeyValuePair kvp in previousContext.BreakpointRequests) - { - context.BreakpointRequests[kvp.Key] = kvp.Value.Clone(); - } - context.PauseOnExceptions = previousContext.PauseOnExceptions; - } - } - protected async Task OnDefaultContext(SessionId sessionId, ExecutionContext context, CancellationToken token) { Log("verbose", "Default context created, clearing state and sending events"); - OnDefaultContextUpdate(sessionId, context); + Contexts.OnDefaultContextUpdate(sessionId, context); if (await IsRuntimeAlreadyReadyAlready(sessionId, token)) await RuntimeReady(sessionId, token); } protected async Task OnResume(MessageId msg_id, CancellationToken token) { - ExecutionContext context = GetContext(msg_id); + ExecutionContext context = Contexts.GetCurrentContext(msg_id); if (context.CallStack != null) { // Stopped on managed code @@ -1219,13 +1204,13 @@ protected async Task OnResume(MessageId msg_id, CancellationToken token) } //discard managed frames - GetContext(msg_id).ClearState(); + Contexts.GetCurrentContext(msg_id).ClearState(); } protected async Task Step(MessageId msgId, StepKind kind, CancellationToken token) { - ExecutionContext context = GetContext(msgId); + ExecutionContext context = Contexts.GetCurrentContext(msgId); if (context.CallStack == null) return false; @@ -1295,7 +1280,7 @@ private async Task OnAssemblyLoadedJSEvent(SessionId sessionId, JObject ev var assembly_data = Convert.FromBase64String(assembly_b64); var pdb_data = string.IsNullOrEmpty(pdb_b64) ? null : Convert.FromBase64String(pdb_b64); - var context = GetContext(sessionId); + var context = Contexts.GetCurrentContext(sessionId); foreach (var source in store.Add(sessionId, assembly_name, assembly_data, pdb_data, token)) { await OnSourceFileAdded(sessionId, source, context, token); @@ -1314,7 +1299,7 @@ private async Task OnSetEntrypointBreakpoint(SessionId sessionId, JObject args, { try { - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); var argsNew = JObject.FromObject(new { @@ -1380,7 +1365,7 @@ private async Task OnEvaluateOnCallFrame(MessageId msg_id, int scopeId, st { try { - ExecutionContext context = GetContext(msg_id); + ExecutionContext context = Contexts.GetCurrentContext(msg_id); if (context.CallStack == null) return false; @@ -1426,7 +1411,7 @@ internal async Task GetScopeProperties(SessionId msg_id, int scopeId, Ca { try { - ExecutionContext context = GetContext(msg_id); + ExecutionContext context = Contexts.GetCurrentContext(msg_id); Frame scope = context.CallStack.FirstOrDefault(s => s.Id == scopeId); if (scope == null) return Result.Err(JObject.FromObject(new { message = $"Could not find scope with id #{scopeId}" })); @@ -1457,7 +1442,7 @@ internal async Task GetScopeProperties(SessionId msg_id, int scopeId, Ca private async Task SetMonoBreakpoint(SessionId sessionId, string reqId, SourceLocation location, string condition, CancellationToken token) { - var context = GetContext(sessionId); + var context = Contexts.GetCurrentContext(sessionId); var bp = new Breakpoint(reqId, location, condition, BreakpointState.Pending); string asm_name = bp.Location.IlLocation.Method.Assembly.Name; int method_token = bp.Location.IlLocation.Method.Token; @@ -1487,14 +1472,23 @@ internal virtual async Task OnSourceFileAdded(SessionId sessionId, SourceFile so { if (req.TryResolve(source)) { - await SetBreakpoint(sessionId, context.store, req, true, false, token); + try + { + await SetBreakpoint(sessionId, context.store, req, true, false, token); + } + catch (DebuggerAgentException e) + { + //it's not a wasm page then the command throws an error + if (!e.Message.Contains("getDotnetRuntime is not defined")) + logger.LogDebug($"Unexpected error on OnSourceFileAdded {e}"); + } } } } internal virtual async Task LoadStore(SessionId sessionId, bool tryUseDebuggerProtocol, CancellationToken token) { - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); if (Interlocked.CompareExchange(ref context.store, new DebugStore(this, logger), null) != null) return await context.Source.Task; @@ -1555,7 +1549,7 @@ protected async Task RuntimeReady(SessionId sessionId, CancellationT { try { - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); if (Interlocked.CompareExchange(ref context.ready, new TaskCompletionSource(), null) != null) return await context.ready.Task; await context.SdbAgent.SendDebuggerAgentCommand(CmdEventRequest.ClearAllBreakpoints, null, token); @@ -1607,7 +1601,7 @@ private static IEnumerable> GetBPReqLocation private async Task ResetBreakpoint(SessionId msg_id, DebugStore store, MethodInfo method, CancellationToken token) { - ExecutionContext context = GetContext(msg_id); + ExecutionContext context = Contexts.GetCurrentContext(msg_id); foreach (var req in context.BreakpointRequests.Values) { if (req.Method != null) @@ -1632,7 +1626,7 @@ protected async Task RemoveBreakpoint(SessionId msg_id, JObject args, bool isEnC { string bpid = args?["breakpointId"]?.Value(); - ExecutionContext context = GetContext(msg_id); + ExecutionContext context = Contexts.GetCurrentContext(msg_id); if (!context.BreakpointRequests.TryGetValue(bpid, out BreakpointRequest breakpointRequest)) return; @@ -1654,7 +1648,7 @@ protected async Task RemoveBreakpoint(SessionId msg_id, JObject args, bool isEnC protected async Task SetBreakpoint(SessionId sessionId, DebugStore store, BreakpointRequest req, bool sendResolvedEvent, bool fromEnC, CancellationToken token) { - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); if ((!fromEnC && req.Locations.Any()) || (fromEnC && req.Locations.Any(bp => bp.State == BreakpointState.Active))) { if (!fromEnC) @@ -1737,7 +1731,7 @@ private static bool IsNestedMethod(DebugStore store, Frame scope, SourceLocation private async Task OnSetNextIP(MessageId sessionId, SourceLocation targetLocation, CancellationToken token) { DebugStore store = await RuntimeReady(sessionId, token); - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); Frame scope = context.CallStack.First(); SourceLocation foundLocation = DebugStore.FindBreakpointLocations(targetLocation, targetLocation, scope.Method.Info) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index 04fd3d8eb61730..4368c4e8730e82 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -1581,7 +1581,7 @@ public async Task GetAssemblyFromType(int type_id, CancellationToken token) public JToken GetEvaluationResultProperties(string id) { - ExecutionContext context = proxy.GetContext(sessionId); + ExecutionContext context = proxy.Contexts.GetCurrentContext(sessionId); var resolver = new MemberReferenceResolver(proxy, context, sessionId, context.CallStack.First().Id, logger); var evaluationResult = resolver.TryGetEvaluationResult(id); return evaluationResult["value"]; @@ -1602,7 +1602,7 @@ public async Task GetValueFromDebuggerDisplayAttribute(DotnetObjectId do var stringId = getCAttrsRetReader.ReadInt32(); var dispAttrStr = await GetStringValue(stringId, token); - ExecutionContext context = proxy.GetContext(sessionId); + ExecutionContext context = proxy.Contexts.GetCurrentContext(sessionId); GetMembersResult members = await GetTypeMemberValues( dotnetObjectId, GetObjectCommandOptions.WithProperties | GetObjectCommandOptions.ForDebuggerDisplayAttribute,