Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wasm][debugger] Detect initial status of pause on exceptions. #54040

Merged
merged 9 commits into from
Jun 25, 2021
3 changes: 3 additions & 0 deletions src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Frame> CallStack { get; set; }

public string[] LoadedFiles { get; set; }
Expand Down
62 changes: 60 additions & 2 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ internal class MonoProxy : DevToolsProxy
private static HttpClient client = new HttpClient();
private HashSet<SessionId> sessions = new HashSet<SessionId>();
private Dictionary<SessionId, ExecutionContext> contexts = new Dictionary<SessionId, ExecutionContext>();
private const string sPauseOnUncaught = "pause_on_uncaught";
private const string sPauseOnCaught = "pause_on_caught";

public MonoProxy(ILoggerFactory loggerFactory, IList<string> urlSymbolServerList) : base(loggerFactory)
{
Expand Down Expand Up @@ -122,8 +124,41 @@ protected override async Task<bool> AcceptEvent(SessionId sessionId, string meth
return true;
}

case "Runtime.exceptionThrown":
{
if (!GetContext(sessionId).IsRuntimeReady)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might need to remove this check if you were trying the on attach version

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attach is the case when the page is already loaded and then we connect the debugger? I tested it and it's working.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only tested debugging on Chrome, I didn't test with VS or VSCode.

{
string exceptionError = args?["exceptionDetails"]?["exception"]?["value"]?.Value<string>();
if (exceptionError == sPauseOnUncaught || exceptionError == sPauseOnCaught)
{
return true;
}
}
break;
}

case "Debugger.paused":
{
if (!GetContext(sessionId).IsRuntimeReady)
{
string reason = args?["reason"]?.Value<string>();
if (reason == "exception")
{
string exceptionError = args?["data"]?["value"]?.Value<string>();
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<string>();
switch (top_func) {
Expand Down Expand Up @@ -398,7 +433,23 @@ protected override async Task<bool> AcceptCommand(MessageId id, string method, J
case "Debugger.setPauseOnExceptions":
{
string state = args["state"].Value<string>();
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;
}
Expand Down Expand Up @@ -1152,6 +1203,11 @@ private async Task<DebugStore> 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);

Expand Down Expand Up @@ -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)
Expand Down
105 changes: 105 additions & 0 deletions src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading.Tasks;
using Microsoft.WebAssembly.Diagnostics;
using Newtonsoft.Json.Linq;
using System.Threading;
using Xunit;

namespace DebuggerTests
Expand Down Expand Up @@ -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<string>(), "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<string>());
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<string>(), "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<string>());
CheckString(exception_members, "message", "not implemented caught");

pause_location = await WaitForManagedException(null);
AssertEqual("run", pause_location["callFrames"]?[0]?["functionName"]?.Value<string>(), "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<string>());
CheckString(exception_members, "message", "not implemented uncaught");
}


async Task<JObject> WaitForManagedException(JObject pause_location)
{
while (true)
Expand Down