diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs index f39f2736caaeb..6f1ec09317e0a 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs @@ -279,6 +279,9 @@ internal class ExecutionContext public int Id { get; set; } public object AuxData { get; set; } + public bool PauseOnUncaught { get; set; } + public bool PauseOnCaught { get; set; } + public List CallStack { get; set; } public string[] LoadedFiles { get; set; } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 689904740d3d6..f45d31ca1c874 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -23,6 +23,8 @@ internal class MonoProxy : DevToolsProxy private static HttpClient client = new HttpClient(); private HashSet sessions = new HashSet(); private Dictionary contexts = new Dictionary(); + private const string sPauseOnUncaught = "pause_on_uncaught"; + private const string sPauseOnCaught = "pause_on_caught"; public MonoProxy(ILoggerFactory loggerFactory, IList urlSymbolServerList) : base(loggerFactory) { @@ -122,8 +124,41 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth return true; } + case "Runtime.exceptionThrown": + { + if (!GetContext(sessionId).IsRuntimeReady) + { + string exceptionError = args?["exceptionDetails"]?["exception"]?["value"]?.Value(); + if (exceptionError == sPauseOnUncaught || exceptionError == sPauseOnCaught) + { + return true; + } + } + break; + } + case "Debugger.paused": { + if (!GetContext(sessionId).IsRuntimeReady) + { + string reason = args?["reason"]?.Value(); + if (reason == "exception") + { + string exceptionError = args?["data"]?["value"]?.Value(); + if (exceptionError == sPauseOnUncaught) + { + await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + GetContext(sessionId).PauseOnUncaught = true; + return true; + } + if (exceptionError == sPauseOnCaught) + { + await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + GetContext(sessionId).PauseOnCaught = true; + return true; + } + } + } //TODO figure out how to stich out more frames and, in particular what happens when real wasm is on the stack string top_func = args?["callFrames"]?[0]?["functionName"]?.Value(); switch (top_func) { @@ -398,7 +433,23 @@ protected override async Task AcceptCommand(MessageId id, string method, J case "Debugger.setPauseOnExceptions": { string state = args["state"].Value(); - await sdbHelper.EnableExceptions(id, state, token); + if (!context.IsRuntimeReady) + { + context.PauseOnCaught = false; + context.PauseOnUncaught = false; + switch (state) + { + case "all": + context.PauseOnCaught = true; + context.PauseOnUncaught = true; + break; + case "uncaught": + context.PauseOnUncaught = true; + break; + } + } + else + await sdbHelper.EnableExceptions(id, state, token); // Pass this on to JS too return false; } @@ -1152,6 +1203,11 @@ private async Task RuntimeReady(SessionId sessionId, CancellationTok Log("verbose", $"Failed to clear breakpoints"); } + if (context.PauseOnCaught && context.PauseOnUncaught) + await sdbHelper.EnableExceptions(sessionId, "all", token); + else if (context.PauseOnUncaught) + await sdbHelper.EnableExceptions(sessionId, "uncaught", token); + await sdbHelper.SetProtocolVersion(sessionId, token); await sdbHelper.EnableReceiveUserBreakRequest(sessionId, token); @@ -1289,10 +1345,12 @@ private async Task AttachToTarget(SessionId sessionId, CancellationToken token) // see https://github.com/mono/mono/issues/19549 for background if (sessions.Add(sessionId)) { + string checkUncaughtExceptions = $"throw \"{sPauseOnUncaught}\";"; + string checkCaughtExceptions = $"try {{throw \"{sPauseOnCaught}\";}} catch {{}}"; await SendMonoCommand(sessionId, new MonoCommands("globalThis.dotnetDebugger = true"), token); Result res = await SendCommand(sessionId, "Page.addScriptToEvaluateOnNewDocument", - JObject.FromObject(new { source = "globalThis.dotnetDebugger = true; delete navigator.constructor.prototype.webdriver" }), + JObject.FromObject(new { source = $"globalThis.dotnetDebugger = true; delete navigator.constructor.prototype.webdriver; {checkCaughtExceptions} {checkUncaughtExceptions}" }), token); if (sessionId != SessionId.Null && !res.IsOk) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs index 1be0b18f325b5..eb0ebe0cc0d54 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.WebAssembly.Diagnostics; using Newtonsoft.Json.Linq; +using System.Threading; using Xunit; namespace DebuggerTests @@ -191,6 +192,110 @@ await CheckValue(pause_location["data"], JObject.FromObject(new CheckString(exception_members, "message", exception_message); } + [Fact] + public async Task ExceptionTestUncaughtWithReload() + { + string entry_method_name = "[debugger-test] DebuggerTests.ExceptionTestsClass:TestExceptions"; + var debugger_test_loc = "dotnet://debugger-test.dll/debugger-exception-test.cs"; + + await SetPauseOnException("uncaught"); + + await SendCommand("Page.enable", null); + await SendCommand("Page.reload", JObject.FromObject(new + { + ignoreCache = true + })); + Thread.Sleep(1000); + + var eval_expr = "window.setTimeout(function() { invoke_static_method (" + + $"'{entry_method_name}'" + + "); }, 1);"; + + var pause_location = await EvaluateAndCheck(eval_expr, null, 0, 0, null); + //stop in the managed caught exception + pause_location = await WaitForManagedException(pause_location); + + AssertEqual("run", pause_location["callFrames"]?[0]?["functionName"]?.Value(), "pause1"); + + //stop in the uncaught exception + CheckLocation(debugger_test_loc, 28, 16, scripts, pause_location["callFrames"][0]["location"]); + + await CheckValue(pause_location["data"], JObject.FromObject(new + { + type = "object", + subtype = "error", + className = "DebuggerTests.CustomException", + uncaught = true + }), "exception1.data"); + + var exception_members = await GetProperties(pause_location["data"]["objectId"]?.Value()); + CheckString(exception_members, "message", "not implemented uncaught"); + } + + [Fact] + public async Task ExceptionTestAllWithReload() + { + string entry_method_name = "[debugger-test] DebuggerTests.ExceptionTestsClass:TestExceptions"; + var debugger_test_loc = "dotnet://debugger-test.dll/debugger-exception-test.cs"; + + await SetPauseOnException("all"); + + await SendCommand("Page.enable", null); + var pause_location = await SendCommandAndCheck(JObject.FromObject(new + { + ignoreCache = true + }), "Page.reload",null, 0, 0, null); + Thread.Sleep(1000); + + //send a lot of resumes to "skip" all the pauses on caught exception and completely reload the page + int i = 0; + while (i < 100) + { + Result res = await cli.SendCommand("Debugger.resume", null, token); + i++; + } + + + var eval_expr = "window.setTimeout(function() { invoke_static_method (" + + $"'{entry_method_name}'" + + "); }, 1);"; + + pause_location = await EvaluateAndCheck(eval_expr, null, 0, 0, null); + //stop in the managed caught exception + pause_location = await WaitForManagedException(pause_location); + + AssertEqual("run", pause_location["callFrames"]?[0]?["functionName"]?.Value(), "pause0"); + + await CheckValue(pause_location["data"], JObject.FromObject(new + { + type = "object", + subtype = "error", + className = "DebuggerTests.CustomException", + uncaught = false + }), "exception0.data"); + + var exception_members = await GetProperties(pause_location["data"]["objectId"]?.Value()); + CheckString(exception_members, "message", "not implemented caught"); + + pause_location = await WaitForManagedException(null); + AssertEqual("run", pause_location["callFrames"]?[0]?["functionName"]?.Value(), "pause1"); + + //stop in the uncaught exception + CheckLocation(debugger_test_loc, 28, 16, scripts, pause_location["callFrames"][0]["location"]); + + await CheckValue(pause_location["data"], JObject.FromObject(new + { + type = "object", + subtype = "error", + className = "DebuggerTests.CustomException", + uncaught = true + }), "exception1.data"); + + exception_members = await GetProperties(pause_location["data"]["objectId"]?.Value()); + CheckString(exception_members, "message", "not implemented uncaught"); + } + + async Task WaitForManagedException(JObject pause_location) { while (true)