diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs index 7860aef3772e8a..e37f75191c5110 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs @@ -333,6 +333,7 @@ internal class MethodInfo public bool IsStatic() => (methodDef.Attributes & MethodAttributes.Static) != 0; public int IsAsync { get; set; } public bool IsHiddenFromDebugger { get; } + public bool HasStepThroughAttribute { get; } public TypeInfo TypeInfo { get; } public MethodInfo(AssemblyInfo assembly, MethodDefinitionHandle methodDefHandle, int token, SourceFile source, TypeInfo type, MetadataReader asmMetadataReader, MetadataReader pdbMetadataReader) @@ -379,7 +380,12 @@ public MethodInfo(AssemblyInfo assembly, MethodDefinitionHandle methodDefHandle, var name = asmMetadataReader.GetString(asmMetadataReader.GetTypeReference((TypeReferenceHandle)container).Name); if (name == "DebuggerHiddenAttribute") { - this.IsHiddenFromDebugger = true; + IsHiddenFromDebugger = true; + break; + } + if (name == "DebuggerStepThroughAttribute") + { + HasStepThroughAttribute = true; break; } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs index e17c3d2bccc4f5..af1cead1c95935 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs @@ -312,6 +312,8 @@ public ExecutionContext(MonoSDBHelper sdbAgent, int id, object auxData) public TaskCompletionSource ready; public bool IsRuntimeReady => ready != null && ready.Task.IsCompleted; public bool IsSkippingHiddenMethod { get; set; } + public bool IsSteppingThroughMethod { get; set; } + public bool IsResumedAfterBp { get; set; } public int ThreadId { get; set; } public int Id { get; set; } public object AuxData { get; set; } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 13979ad0f4968d..33d1dc4c59d985 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -26,6 +26,7 @@ internal class MonoProxy : DevToolsProxy private const string sPauseOnCaught = "pause_on_caught"; // index of the runtime in a same JS page/process public int RuntimeId { get; private init; } + public bool JustMyCode { get; private set; } public MonoProxy(ILoggerFactory loggerFactory, IList urlSymbolServerList, int runtimeId = 0) : base(loggerFactory) { @@ -573,10 +574,33 @@ protected override async Task AcceptCommand(MessageId id, string method, J return true; } } + case "DotnetDebugger.justMyCode": + { + try + { + SetJustMyCode(id, args, token); + } + catch (Exception) + { + SendResponse(id, + Result.Exception(new ArgumentException( + $"DotnetDebugger.justMyCode got incorrect argument ({args})")), + token); + } + return true; + } } return false; } + private void SetJustMyCode(MessageId id, JObject args, CancellationToken token) + { + var isEnabled = args["enabled"]?.Value(); + if (isEnabled == null) + throw new ArgumentException(); + JustMyCode = isEnabled.Value; + SendResponse(id, Result.OkFromObject(new { justMyCodeEnabled = JustMyCode }), token); + } private async Task CallOnFunction(MessageId id, JObject args, CancellationToken token) { var context = GetContext(id); @@ -813,24 +837,44 @@ private async Task SendCallStack(SessionId sessionId, ExecutionContext con DebugStore store = await LoadStore(sessionId, token); var method = await context.SdbAgent.GetMethodInfo(methodId, token); - if (context.IsSkippingHiddenMethod == true) + var shouldReturn = await SkipMethod( + isSkippable: context.IsSkippingHiddenMethod, + shouldBeSkipped: event_kind != EventKind.UserBreak, + StepKind.Over); + context.IsSkippingHiddenMethod = false; + if (shouldReturn) + return true; + + shouldReturn = await SkipMethod( + isSkippable: context.IsSteppingThroughMethod, + shouldBeSkipped: event_kind != EventKind.UserBreak && event_kind != EventKind.Breakpoint, + StepKind.Over); + context.IsSteppingThroughMethod = false; + if (shouldReturn) + return true; + + if (j == 0 && (method?.Info.HasStepThroughAttribute == true || method?.Info.IsHiddenFromDebugger == true)) { - context.IsSkippingHiddenMethod = false; - if (event_kind != EventKind.UserBreak) + if (method.Info.IsHiddenFromDebugger) { - await context.SdbAgent.Step(context.ThreadId, StepKind.Over, token); - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); - return true; + if (event_kind == EventKind.Step) + context.IsSkippingHiddenMethod = true; + if (await SkipMethod(isSkippable: true, shouldBeSkipped: true, StepKind.Out)) + return true; } - } - if (j == 0 && method?.Info.IsHiddenFromDebugger == true) - { - if (event_kind == EventKind.Step) - context.IsSkippingHiddenMethod = true; - await context.SdbAgent.Step(context.ThreadId, StepKind.Out, token); - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); - return true; + if (event_kind == EventKind.Step || + (JustMyCode && (event_kind == EventKind.Breakpoint || event_kind == EventKind.UserBreak))) + { + if (context.IsResumedAfterBp) + context.IsResumedAfterBp = false; + else if (event_kind != EventKind.UserBreak) + context.IsSteppingThroughMethod = true; + if (await SkipMethod(isSkippable: true, shouldBeSkipped: true, StepKind.Out)) + return true; + } + if (event_kind == EventKind.Breakpoint) + context.IsResumedAfterBp = true; } SourceLocation location = method?.Info.GetLocationByIl(il_pos); @@ -910,6 +954,17 @@ private async Task SendCallStack(SessionId sessionId, ExecutionContext con SendEvent(sessionId, "Debugger.paused", o, token); return true; + + async Task SkipMethod(bool isSkippable, bool shouldBeSkipped, StepKind stepKind) + { + if (isSkippable && shouldBeSkipped) + { + await context.SdbAgent.Step(context.ThreadId, stepKind, token); + await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + return true; + } + return false; + } } private async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObject args, CancellationToken token) { diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs index 2644e8f7540404..01f526e8c78db1 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs @@ -668,7 +668,7 @@ await EvaluateAndCheck( [Fact] - public async Task DebuggerAttributeNoStopInDebuggerHidden() + public async Task DebuggerHiddenNoStopOnBp() { var bp_hidden = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "HiddenMethod", 1); var bp_visible = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "VisibleMethod", 1); @@ -683,7 +683,7 @@ await EvaluateAndCheck( } [Fact] - public async Task DebuggerAttributeStopOnDebuggerHiddenCallWithDebuggerBreakCall() + public async Task DebuggerHiddenStopOnUserBp() { var bp_init = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "RunDebuggerBreak", 0); var init_location = await EvaluateAndCheck( @@ -708,6 +708,131 @@ await SendCommandAndCheck(null, "Debugger.resume", "VisibleMethodDebuggerBreak"); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task StepThroughAttributeStepInNoBp(bool justMyCodeEnabled) + { + var bp_init = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "RunStepThrough", 1); + if (justMyCodeEnabled) + await SetJustMyCode(true); + + var init_location = await EvaluateAndCheck( + "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerAttribute:RunStepThrough'); }, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", + bp_init.Value["locations"][0]["lineNumber"].Value(), + bp_init.Value["locations"][0]["columnNumber"].Value(), + "RunStepThrough" + ); + await SendCommandAndCheck(null, "Debugger.stepInto", "dotnet://debugger-test.dll/debugger-test.cs", 868, 8, "RunStepThrough"); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task StepThroughAttributeStepInWithBp(bool justMyCodeEnabled) + { + var bp_init = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "RunStepThrough", 1); + var bp1_decorated_fun = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "NotStopOnJustMyCode", 1); + var bp2_decorated_fun = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "NotStopOnJustMyCode", 3); + + var init_location = await EvaluateAndCheck( + "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerAttribute:RunStepThrough'); }, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", + bp_init.Value["locations"][0]["lineNumber"].Value(), + bp_init.Value["locations"][0]["columnNumber"].Value(), + "RunStepThrough" + ); + + if (justMyCodeEnabled) + { + await SetJustMyCode(true); + await SendCommandAndCheck(null, "Debugger.stepInto", "dotnet://debugger-test.dll/debugger-test.cs", 868, 8, "RunStepThrough"); + } + else + { + var line1 = bp1_decorated_fun.Value["locations"][0]["lineNumber"].Value(); + var line2 = bp2_decorated_fun.Value["locations"][0]["lineNumber"].Value(); + var line3 = 867; + var step_throgh_fun = "NotStopOnJustMyCode"; + var outer_fun = "RunStepThrough"; + await SendCommandAndCheck(null, "Debugger.stepInto", "dotnet://debugger-test.dll/debugger-test.cs", line1, 8, step_throgh_fun); + await SendCommandAndCheck(null, "Debugger.stepInto", "dotnet://debugger-test.dll/debugger-test.cs", line2, 8, step_throgh_fun); + await SendCommandAndCheck(null, "Debugger.stepInto", "dotnet://debugger-test.dll/debugger-test.cs", line3, 8, outer_fun); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task StepThroughAttributeResumeWithBp(bool justMyCodeEnabled) + { + var bp_init = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "RunStepThrough", 1); + var bp1_decorated_fun = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "NotStopOnJustMyCode", 1); + var bp_outside_decorated_fun = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "RunStepThrough", 2); + + var init_location = await EvaluateAndCheck( + "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerAttribute:RunStepThrough'); }, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", + bp_init.Value["locations"][0]["lineNumber"].Value(), + bp_init.Value["locations"][0]["columnNumber"].Value(), + "RunStepThrough" + ); + + if (justMyCodeEnabled) + await SetJustMyCode(true); + else + { + var line1 = bp1_decorated_fun.Value["locations"][0]["lineNumber"].Value(); + var function_name1 = "NotStopOnJustMyCode"; + await SendCommandAndCheck(null, "Debugger.resume", "dotnet://debugger-test.dll/debugger-test.cs", line1, 8, function_name1); + } + + var line2 = bp_outside_decorated_fun.Value["locations"][0]["lineNumber"].Value(); + var function_name2 = "RunStepThrough"; + await SendCommandAndCheck(null, "Debugger.resume", "dotnet://debugger-test.dll/debugger-test.cs", line2, 8, function_name2); + } + + [Theory] + [InlineData(false, "Debugger.resume")] + [InlineData(false, "Debugger.stepInto")] + [InlineData(true, "Debugger.stepInto")] + [InlineData(true, "Debugger.resume")] + public async Task StepThroughAttributeWithUserBp(bool justMyCodeEnabled, string debuggingFunction) + { + var bp_init = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "RunStepThrough", 2); + var bp_outside_decorated_fun = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "RunStepThrough", 3); + + var init_location = await EvaluateAndCheck( + "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerAttribute:RunStepThrough'); }, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", + bp_init.Value["locations"][0]["lineNumber"].Value(), + bp_init.Value["locations"][0]["columnNumber"].Value(), + "RunStepThrough" + ); + + int line1, line2; + string function_name1, function_name2; + + if (justMyCodeEnabled) + { + await SetJustMyCode(true); + line1 = bp_outside_decorated_fun.Value["locations"][0]["lineNumber"].Value() - 1; + function_name1 = "RunStepThrough"; + line2 = bp_outside_decorated_fun.Value["locations"][0]["lineNumber"].Value(); + function_name2 = "RunStepThrough"; + } + else + { + line1 = 862; + function_name1 = "NotStopOnJustMyCodeUserBp"; + line2 = bp_outside_decorated_fun.Value["locations"][0]["lineNumber"].Value(); + function_name2 = "RunStepThrough"; + } + await SendCommandAndCheck(null, debuggingFunction, "dotnet://debugger-test.dll/debugger-test.cs", line1, 8, function_name1); + await SendCommandAndCheck(null, debuggingFunction, "dotnet://debugger-test.dll/debugger-test.cs", line2, 4, function_name2); + } + [Fact] public async Task CreateGoodBreakpointAndHitGoToWasmPageWithoutAssetsComeBackAndHitAgain() { @@ -776,7 +901,5 @@ await EvaluateAndCheck( } ); } - - } } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs index 5660e8c55011b3..d49a2331e2a11e 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs @@ -1237,6 +1237,14 @@ internal async Task LoadAssemblyAndTestHotReload(string asm_file, strin await cli.SendCommand("Runtime.evaluate", run_method, token); return await insp.WaitFor(Inspector.PAUSE); } + + internal async Task SetJustMyCode(bool enabled) + { + var req = JObject.FromObject(new { enabled = enabled }); + var res = await cli.SendCommand("DotnetDebugger.justMyCode", req, token); + Assert.True(res.IsOk); + Assert.Equal(res.Value["justMyCodeEnabled"], enabled); + } } class DotnetObjectId diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs index bb61841cdd1f7e..9bb227f43f4092 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs @@ -913,7 +913,7 @@ public async Task InspectLocalsUsingClassFromLibraryUsingDebugTypeFull() await EvaluateAndCheck( "window.setTimeout(function() {" + expression + "; }, 1);", - "dotnet://debugger-test.dll/debugger-test.cs", 860, 8, + "dotnet://debugger-test.dll/debugger-test.cs", 880, 8, "CallToEvaluateLocal", wait_for_event_fn: async (pause_location) => { diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs index 98b9cb00dc4015..49c811faaa317f 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs @@ -848,6 +848,26 @@ public static void RunDebuggerBreak() HiddenMethodDebuggerBreak(); VisibleMethodDebuggerBreak(); } + + [System.Diagnostics.DebuggerStepThroughAttribute] + public static void NotStopOnJustMyCode() + { + var a = 0; + currentCount++; + var b = 1; + } + + [System.Diagnostics.DebuggerStepThroughAttribute] + public static void NotStopOnJustMyCodeUserBp() + { + System.Diagnostics.Debugger.Break(); + } + + public static void RunStepThrough() + { + NotStopOnJustMyCode(); + NotStopOnJustMyCodeUserBp(); + } } public class DebugTypeFull