Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -288,6 +289,7 @@ internal class ExecutionContext
public object AuxData { get; set; }

public PauseOnExceptionsKind PauseOnExceptions { get; set; }
internal bool Destroyed { get; set; }

public List<Frame> CallStack { get; set; }

Expand Down Expand Up @@ -341,4 +343,70 @@ public PerScopeCache()
{
}
}

internal sealed class ConcurrentExecutionContextDictionary
{
private ConcurrentDictionary<SessionId, ConcurrentBag<ExecutionContext>> contexts = new ConcurrentDictionary<SessionId, ConcurrentBag<ExecutionContext>>();
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<ExecutionContext> contextBag))
return false;
if (contextBag.IsEmpty)
return false;
IEnumerable<ExecutionContext> 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<string, BreakpointRequest> 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<ExecutionContext> bag = contexts.GetOrAdd(sessionId, _ => new ConcurrentBag<ExecutionContext>());
bag.Add(newExecutionContext);
return hasExisting;
}

public void DestroyContext(SessionId sessionId, int id)
{
if (!contexts.TryGetValue(sessionId, out ConcurrentBag<ExecutionContext> 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<ExecutionContext> contextBag))
return;
foreach (ExecutionContext context in contextBag)
context.Destroyed = true;
}

public bool ContainsKey(SessionId sessionId) => contexts.ContainsKey(sessionId);
}
}
89 changes: 43 additions & 46 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal class MonoProxy : DevToolsProxy
private IList<string> urlSymbolServerList;
private static HttpClient client = new HttpClient();
private HashSet<SessionId> sessions = new HashSet<SessionId>();
private Dictionary<SessionId, ExecutionContext> contexts = new Dictionary<SessionId, ExecutionContext>();
internal ConcurrentExecutionContextDictionary Contexts = new ConcurrentExecutionContextDictionary();
private const string sPauseOnUncaught = "pause_on_uncaught";
private const string sPauseOnCaught = "pause_on_caught";

Expand All @@ -32,21 +32,6 @@ public MonoProxy(ILoggerFactory loggerFactory, IList<string> 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<Result> SendMonoCommand(SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand(id, "Runtime.evaluate", JObject.FromObject(cmd), token);

protected override async Task<bool> AcceptEvent(SessionId sessionId, string method, JObject args, CancellationToken token)
Expand All @@ -56,7 +41,7 @@ protected override async Task<bool> 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")
Expand Down Expand Up @@ -129,7 +114,7 @@ protected override async Task<bool> 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)
Expand All @@ -141,10 +126,22 @@ protected override async Task<bool> AcceptEvent(SessionId sessionId, string meth
break;
}

case "Runtime.executionContextDestroyed":
{
Contexts.DestroyContext(sessionId, args["executionContextId"].Value<int>());
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<JArray>()?.Count == 0)
Expand Down Expand Up @@ -238,7 +235,7 @@ protected override async Task<bool> AcceptEvent(SessionId sessionId, string meth

private async Task<bool> 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);
Expand All @@ -252,7 +249,7 @@ protected override async Task<bool> 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);
Expand Down Expand Up @@ -607,7 +604,7 @@ private async Task<bool> CallOnFunction(MessageId id, JObject args, Cancellation

private async Task<bool> 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;
Expand Down Expand Up @@ -844,7 +841,7 @@ private async Task<bool> 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<string>());
var retDebuggerCmd = new MemoryStream(newBytes);
var retDebuggerCmdReader = new MonoBinaryReader(retDebuggerCmd);
Expand Down Expand Up @@ -910,7 +907,7 @@ private async Task<bool> OnReceiveDebuggerAgentEvent(SessionId sessionId, JObjec

internal async Task<MethodInfo> 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)
Expand Down Expand Up @@ -961,22 +958,15 @@ internal async Task<MethodInfo> 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<string, BreakpointRequest> 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);
}

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
Expand All @@ -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<bool> 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;

Expand Down Expand Up @@ -1061,7 +1051,7 @@ private async Task<bool> 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);
Expand All @@ -1080,7 +1070,7 @@ private async Task<bool> 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;

Expand Down Expand Up @@ -1121,7 +1111,7 @@ internal async Task<Result> 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}" }));
Expand Down Expand Up @@ -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<DebugStore> 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;
Expand Down Expand Up @@ -1239,7 +1236,7 @@ async Task<string[]> GetLoadedFiles(SessionId sessionId, ExecutionContext contex

private async Task<DebugStore> RuntimeReady(SessionId sessionId, CancellationToken token)
{
ExecutionContext context = GetContext(sessionId);
ExecutionContext context = Contexts.GetCurrentContext(sessionId);
if (Interlocked.CompareExchange(ref context.ready, new TaskCompletionSource<DebugStore>(), null) != null)
return await context.ready.Task;

Expand All @@ -1263,7 +1260,7 @@ private async Task<DebugStore> 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)
Expand All @@ -1279,7 +1276,7 @@ private async Task RemoveBreakpoint(SessionId msg_id, JObject args, bool isEnCRe
{
string bpid = args?["breakpointId"]?.Value<string>();

ExecutionContext context = GetContext(msg_id);
ExecutionContext context = Contexts.GetCurrentContext(msg_id);
if (!context.BreakpointRequests.TryGetValue(bpid, out BreakpointRequest breakpointRequest))
return;

Expand All @@ -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}");
Expand All @@ -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<Breakpoint>();

Expand Down Expand Up @@ -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 {{}}";
Expand Down
2 changes: 1 addition & 1 deletion src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1408,7 +1408,7 @@ public async Task<string> 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<string>(value: "", type: "object", description: "", writable: false, objectId: $"dotnet:object:{objectId}");
Expand Down