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}");