diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs index 950acbb2dcf915..aa33f94f5cd541 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; @@ -288,6 +289,7 @@ internal class ExecutionContext public object AuxData { get; set; } public PauseOnExceptionsKind PauseOnExceptions { get; set; } + internal bool Destroyed { get; set; } public List CallStack { get; set; } @@ -341,4 +343,70 @@ public PerScopeCache() { } } + + internal sealed class ConcurrentExecutionContextDictionary + { + private ConcurrentDictionary> contexts = new ConcurrentDictionary>(); + 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/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 7b2cc2bff517b1..36a6d8eea56fec 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -22,7 +22,7 @@ internal class MonoProxy : DevToolsProxy private IList urlSymbolServerList; private static HttpClient client = new HttpClient(); private HashSet sessions = new HashSet(); - private Dictionary contexts = new Dictionary(); + internal ConcurrentExecutionContextDictionary Contexts = new ConcurrentExecutionContextDictionary(); private const string sPauseOnUncaught = "pause_on_uncaught"; private const string sPauseOnCaught = "pause_on_caught"; @@ -32,21 +32,6 @@ public MonoProxy(ILoggerFactory loggerFactory, IList urlSymbolServerList SdbHelper = new MonoSDBHelper(this, logger); } - 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 Task SendMonoCommand(SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand(id, "Runtime.evaluate", JObject.FromObject(cmd), token); protected override async Task AcceptEvent(SessionId sessionId, string method, JObject args, CancellationToken token) @@ -56,7 +41,7 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth 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") @@ -129,7 +114,7 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth case "Runtime.exceptionThrown": { // 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; if (!context.IsRuntimeReady) @@ -141,10 +126,22 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth break; } + 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.TryGetValue(sessionId, out ExecutionContext context)) + if (!Contexts.TryGetCurrentExecutionContextValue(sessionId, out ExecutionContext context)) return false; if (args?["callFrames"]?.Value()?.Count == 0) @@ -238,7 +235,7 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth private 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(), token); @@ -252,7 +249,7 @@ protected override async Task AcceptCommand(MessageId id, string method, J if (id == SessionId.Null) await AttachToTarget(id, token); - if (!contexts.TryGetValue(id, out ExecutionContext context)) + if (!Contexts.TryGetCurrentExecutionContextValue(id, out ExecutionContext context)) { // for Dotnetdebugger.* messages, treat them as handled, thus not passing them on to the browser return method.StartsWith("DotnetDebugger.", StringComparison.OrdinalIgnoreCase); @@ -607,7 +604,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 ctx = GetContext(id); + ExecutionContext ctx = Contexts.GetCurrentContext(id); Frame scope = ctx.CallStack.FirstOrDefault(s => s.Id == scopeId); if (scope == null) return false; @@ -844,7 +841,7 @@ private async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObjec if (res.IsErr) return false; - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); byte[] newBytes = Convert.FromBase64String(res.Value?["result"]?["value"]?["value"]?.Value()); var retDebuggerCmd = new MemoryStream(newBytes); var retDebuggerCmdReader = new MonoBinaryReader(retDebuggerCmd); @@ -910,7 +907,7 @@ private async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObjec 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) @@ -961,14 +958,7 @@ internal async Task LoadSymbolsOnDemand(AssemblyInfo asm, int method private async Task OnDefaultContext(SessionId sessionId, ExecutionContext context, CancellationToken token) { Log("verbose", "Default context created, clearing state and sending events"); - if (UpdateContext(sessionId, context, out ExecutionContext previousContext)) - { - foreach (KeyValuePair kvp in previousContext.BreakpointRequests) - { - context.BreakpointRequests[kvp.Key] = kvp.Value.Clone(); - } - context.PauseOnExceptions = previousContext.PauseOnExceptions; - } + Contexts.OnDefaultContextUpdate(sessionId, context); if (await IsRuntimeAlreadyReadyAlready(sessionId, token)) await RuntimeReady(sessionId, token); @@ -976,7 +966,7 @@ private async Task OnDefaultContext(SessionId sessionId, ExecutionContext contex private async Task OnResume(MessageId msg_id, CancellationToken token) { - ExecutionContext ctx = GetContext(msg_id); + ExecutionContext ctx = Contexts.GetCurrentContext(msg_id); if (ctx.CallStack != null) { // Stopped on managed code @@ -985,12 +975,12 @@ private async Task OnResume(MessageId msg_id, CancellationToken token) //discard managed frames SdbHelper.ClearCache(); - GetContext(msg_id).ClearState(); + Contexts.GetCurrentContext(msg_id).ClearState(); } private async Task Step(MessageId msg_id, StepKind kind, CancellationToken token) { - ExecutionContext context = GetContext(msg_id); + ExecutionContext context = Contexts.GetCurrentContext(msg_id); if (context.CallStack == null) return false; @@ -1061,7 +1051,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_data, pdb_data)) { await OnSourceFileAdded(sessionId, source, context, token); @@ -1080,7 +1070,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; @@ -1121,7 +1111,7 @@ internal async Task GetScopeProperties(SessionId msg_id, int scopeId, Ca { try { - ExecutionContext ctx = GetContext(msg_id); + ExecutionContext ctx = Contexts.GetCurrentContext(msg_id); Frame scope = ctx.CallStack.FirstOrDefault(s => s.Id == scopeId); if (scope == null) return Result.Err(JObject.FromObject(new { message = $"Could not find scope with id #{scopeId}" })); @@ -1180,14 +1170,21 @@ private async Task OnSourceFileAdded(SessionId sessionId, SourceFile source, Exe { if (req.TryResolve(source)) { - await SetBreakpoint(sessionId, context.store, req, true, token); + try + { + await SetBreakpoint(sessionId, context.store, req, true, token); + } + catch (Exception e) + { + logger.LogDebug($"Unexpected error on OnSourceFileAdded {e}"); + } } } } internal async Task LoadStore(SessionId sessionId, CancellationToken token) { - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); if (Interlocked.CompareExchange(ref context.store, new DebugStore(logger), null) != null) return await context.Source.Task; @@ -1239,7 +1236,7 @@ async Task GetLoadedFiles(SessionId sessionId, ExecutionContext contex private async Task RuntimeReady(SessionId sessionId, CancellationToken token) { - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); if (Interlocked.CompareExchange(ref context.ready, new TaskCompletionSource(), null) != null) return await context.ready.Task; @@ -1263,7 +1260,7 @@ private async Task RuntimeReady(SessionId sessionId, CancellationTok private async Task ResetBreakpoint(SessionId msg_id, 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) @@ -1279,7 +1276,7 @@ private async Task RemoveBreakpoint(SessionId msg_id, JObject args, bool isEnCRe { 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; @@ -1303,7 +1300,7 @@ private async Task RemoveBreakpoint(SessionId msg_id, JObject args, bool isEnCRe private async Task SetBreakpoint(SessionId sessionId, DebugStore store, BreakpointRequest req, bool sendResolvedEvent, CancellationToken token) { - ExecutionContext context = GetContext(sessionId); + ExecutionContext context = Contexts.GetCurrentContext(sessionId); if (req.Locations.Any()) { Log("debug", $"locations already loaded for {req.Id}"); @@ -1319,7 +1316,7 @@ private async Task SetBreakpoint(SessionId sessionId, DebugStore store, Breakpoi .OrderBy(l => l.Column) .GroupBy(l => l.Id); - logger.LogDebug("BP request for '{req}' runtime ready {context.RuntimeReady}", req, GetContext(sessionId).IsRuntimeReady); + logger.LogDebug("BP request for '{req}' runtime ready {context.RuntimeReady}", req, Contexts.GetCurrentContext(sessionId).IsRuntimeReady); var breakpoints = new List(); @@ -1415,7 +1412,7 @@ private async Task AttachToTarget(SessionId sessionId, CancellationToken token) //we only need this check if it's a non-vs debugging if (sessionId == SessionId.Null) { - if (!contexts.TryGetValue(sessionId, out ExecutionContext context) || context.PauseOnExceptions == PauseOnExceptionsKind.Unset) + if (!Contexts.TryGetCurrentExecutionContextValue(sessionId, out ExecutionContext context) || context.PauseOnExceptions == PauseOnExceptionsKind.Unset) { checkUncaughtExceptions = $"throw \"{sPauseOnUncaught}\";"; checkCaughtExceptions = $"try {{throw \"{sPauseOnCaught}\";}} catch {{}}"; diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index 5390c58d5a3f5a..614bbd4738a46a 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -1408,7 +1408,7 @@ public async Task GetValueFromDebuggerDisplayAttribute(SessionId session var stringId = getCAttrsRetReader.ReadInt32(); var dispAttrStr = await GetStringValue(sessionId, stringId, token); - ExecutionContext context = proxy.GetContext(sessionId); + ExecutionContext context = proxy.Contexts.GetCurrentContext(sessionId); JArray objectValues = await GetObjectValues(sessionId, objectId, GetObjectCommandOptions.WithProperties | GetObjectCommandOptions.ForDebuggerDisplayAttribute, token); var thisObj = CreateJObject(value: "", type: "object", description: "", writable: false, objectId: $"dotnet:object:{objectId}");