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
66 changes: 58 additions & 8 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ internal class MonoSDBHelper
private static int MINOR_VERSION = 61;
private static int MAJOR_VERSION = 2;
private readonly ILogger logger;
private Regex regexForAsyncLocals = new Regex(@"\<([^)]*)\>", RegexOptions.Singleline);

public MonoSDBHelper(MonoProxy proxy, ILogger logger)
{
Expand Down Expand Up @@ -1710,6 +1711,62 @@ public async Task<bool> IsAsyncMethod(SessionId sessionId, int methodId, Cancell
return retDebuggerCmdReader.ReadByte() == 1 ; //token
}

private bool IsClosureReferenceField (string fieldName)
{
// mcs is "$locvar"
// old mcs is "<>f__ref"
// csc is "CS$<>"
// roslyn is "<>8__"
return fieldName.StartsWith ("CS$<>", StringComparison.Ordinal) ||
fieldName.StartsWith ("<>f__ref", StringComparison.Ordinal) ||
fieldName.StartsWith ("$locvar", StringComparison.Ordinal) ||
fieldName.StartsWith ("<>8__", StringComparison.Ordinal);
}

public async Task<JArray> GetHoistedLocalVariables(SessionId sessionId, int objectId, JArray asyncLocals, CancellationToken token)
{
JArray asyncLocalsFull = new JArray();
List<int> objectsAlreadyRead = new();
objectsAlreadyRead.Add(objectId);
foreach (var asyncLocal in asyncLocals)
{
var fieldName = asyncLocal["name"].Value<string>();
if (fieldName.EndsWith("__this", StringComparison.Ordinal))
{
asyncLocal["name"] = "this";
asyncLocalsFull.Add(asyncLocal);
}
else if (IsClosureReferenceField(fieldName)) //same code that has on debugger-libs
{
if (DotnetObjectId.TryParse(asyncLocal?["value"]?["objectId"]?.Value<string>(), out DotnetObjectId dotnetObjectId))
{
if (int.TryParse(dotnetObjectId.Value, out int objectIdToGetInfo) && !objectsAlreadyRead.Contains(objectIdToGetInfo))
{
var asyncLocalsFromObject = await GetObjectValues(sessionId, objectIdToGetInfo, true, false, false, false, token);
var hoistedLocalVariable = await GetHoistedLocalVariables(sessionId, objectIdToGetInfo, asyncLocalsFromObject, token);
asyncLocalsFull = new JArray(asyncLocalsFull.Union(hoistedLocalVariable));
}
}
}
else if (fieldName.StartsWith("<>", StringComparison.Ordinal)) //examples: <>t__builder, <>1__state
{
continue;
}
else if (fieldName.StartsWith('<')) //examples: <code>5__2
{
var match = regexForAsyncLocals.Match(fieldName);
if (match.Success)
asyncLocal["name"] = match.Groups[1].Value;
asyncLocalsFull.Add(asyncLocal);
Copy link
Member

Choose a reason for hiding this comment

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

Would this still be valid without a name? If not, then we shouldn't add it to asyncLocalsFull, and just skip it.

}
else
{
asyncLocalsFull.Add(asyncLocal);
}
}
return asyncLocalsFull;
}

public async Task<JArray> StackFrameGetValues(SessionId sessionId, MethodInfo method, int thread_id, int frame_id, VarInfo[] varIds, CancellationToken token)
{
var commandParams = new MemoryStream();
Expand All @@ -1729,14 +1786,7 @@ public async Task<JArray> StackFrameGetValues(SessionId sessionId, MethodInfo me
retDebuggerCmdReader.ReadByte(); //ignore type
var objectId = retDebuggerCmdReader.ReadInt32();
var asyncLocals = await GetObjectValues(sessionId, objectId, true, false, false, false, token);
Copy link
Member

Choose a reason for hiding this comment

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

Just curious, what does this return? reference to the generated class?

asyncLocals = new JArray(asyncLocals.Where( asyncLocal => !asyncLocal["name"].Value<string>().Contains("<>") || asyncLocal["name"].Value<string>().EndsWith("__this")));
foreach (var asyncLocal in asyncLocals)
{
if (asyncLocal["name"].Value<string>().EndsWith("__this"))
asyncLocal["name"] = "this";
else if (asyncLocal["name"].Value<string>().Contains('<'))
asyncLocal["name"] = Regex.Match(asyncLocal["name"].Value<string>(), @"\<([^)]*)\>").Groups[1].Value;
}
asyncLocals = await GetHoistedLocalVariables(sessionId, objectId, asyncLocals, token);
return asyncLocals;
}

Expand Down
82 changes: 82 additions & 0 deletions src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.WebAssembly.Diagnostics;
using Newtonsoft.Json.Linq;
using Xunit;

namespace DebuggerTests
{
public class AsyncTests : DebuggerTestBase
{

// FIXME: method with multiple async blocks - so that we have two separate classes for that method!
// FIXME: nested blocks
// FIXME: Confirm the actual bp location
// FIXME: check object properties..

//FIXME: function name
[Theory]
[InlineData("ContinueWithStaticAsync", "<ContinueWithStaticAsync>b__3_0")]
[InlineData("ContinueWithInstanceAsync", "<ContinueWithInstanceAsync>b__5_0")]
public async Task AsyncLocalsInContinueWith(string method_name, string expected_method_name) => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.AsyncTests.ContinueWithTests", method_name, 5, expected_method_name,
"window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
wait_for_event_fn: async (pause_location) =>
{
var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
await CheckProps(frame_locals, new
{
t = TObject("System.Threading.Tasks.Task.DelayPromise"),
code = TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"),
@this = TObject("DebuggerTests.AsyncTests.ContinueWithTests.<>c"),
dt = TDateTime(new DateTime(4513, 4, 5, 6, 7, 8))
}, "locals");

var res = await InvokeGetter(GetAndAssertObjectWithName(frame_locals, "t"), "Status");
await CheckValue(res.Value["result"], TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"), "t.Status");
});

[Fact]
public async Task AsyncLocalsInContinueWithInstanceUsingThisBlock() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.AsyncTests.ContinueWithTests", "ContinueWithInstanceUsingThisAsync", 5, "<ContinueWithInstanceUsingThisAsync>b__6_0",
"window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
wait_for_event_fn: async (pause_location) =>
{
var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
await CheckProps(frame_locals, new
{
t = TObject("System.Threading.Tasks.Task.DelayPromise"),
code = TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"),
dt = TDateTime(new DateTime(4513, 4, 5, 6, 7, 8)),
@this = TObject("DebuggerTests.AsyncTests.ContinueWithTests")
}, "locals");

var res = await InvokeGetter(GetAndAssertObjectWithName(frame_locals, "t"), "Status");
await CheckValue(res.Value["result"], TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"), "t.Status");

res = await InvokeGetter(GetAndAssertObjectWithName(frame_locals, "this"), "Date");
await CheckValue(res.Value["result"], TDateTime(new DateTime(2510, 1, 2, 3, 4, 5)), "this.Date");
});

[Fact] // NestedContinueWith
public async Task AsyncLocalsInNestedContinueWithStaticBlock() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.AsyncTests.ContinueWithTests", "NestedContinueWithStaticAsync", 5, "MoveNext",
"window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
wait_for_event_fn: async (pause_location) =>
{
var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
await CheckProps(frame_locals, new
{
t = TObject("System.Threading.Tasks.Task.DelayPromise"),
code = TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"),
str = TString("foobar"),
@this = TObject("DebuggerTests.AsyncTests.ContinueWithTests.<>c__DisplayClass4_0"),
ncs_dt0 = TDateTime(new DateTime(3412, 4, 6, 8, 0, 2))
}, "locals");
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,27 @@ public async Task EvaluateStaticClassInvalidField() => await CheckInspectLocalsA
(_, res) = await EvaluateOnCallFrame(id, "DebuggerTests.InvalidEvaluateStaticClass.StaticProperty2", expect_ok: false);
AssertEqual("Failed to resolve member access for DebuggerTests.InvalidEvaluateStaticClass.StaticProperty2", res.Error["result"]?["description"]?.Value<string>(), "wrong error message");
});

[Fact]
public async Task AsyncLocalsInContinueWithBlock() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.AsyncTests.ContinueWithTests", "ContinueWithStaticAsync", 4, "<ContinueWithStaticAsync>b__3_0",
"window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
wait_for_event_fn: async (pause_location) =>
{
var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();

await EvaluateOnCallFrameAndCheck(id,
($"t.Status", TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion")),
($" t.Status", TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"))
);

await EvaluateOnCallFrameFail(id,
("str", "ReferenceError"),
(" str", "ReferenceError")
);
});

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ public async Task GetObjectValueWithInheritance()
{
var pause_location = await EvaluateAndCheck(
"window.setTimeout(function() { invoke_static_method('[debugger-test] TestChild:TestWatchWithInheritance'); }, 1);",
"dotnet://debugger-test.dll/debugger-test2.cs", 125, 8,
"dotnet://debugger-test.dll/debugger-test2.cs", 127, 8,
"TestWatchWithInheritance");
var frame_id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
var frame_locals = await GetProperties(frame_id);
Expand Down
3 changes: 2 additions & 1 deletion src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -830,12 +830,13 @@ public async Task GetSourceUsingSourceLink()
public async Task InspectTaskAtLocals() => await CheckInspectLocalsAtBreakpointSite(
"InspectTask",
"RunInspectTask",
7,
10,
"<RunInspectTask>b__0" ,
$"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] InspectTask:RunInspectTask'); }}, 1);",
wait_for_event_fn: async (pause_location) =>
{
var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
CheckNumber(locals, "a", 10);

var t_props = await GetObjectOnLocals(locals, "t");
await CheckProps(t_props, new
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

namespace DebuggerTests.AsyncTests
{
public class ContinueWithTests
{
public DateTime Date => new DateTime(2510, 1, 2, 3, 4, 5);

public static async Task RunAsync()
{
await ContinueWithStaticAsync("foobar");
await new ContinueWithTests().ContinueWithInstanceAsync("foobar");

await NestedContinueWithStaticAsync("foobar");
await new ContinueWithTests().NestedContinueWithInstanceAsync("foobar");
await new ContinueWithTests().ContinueWithInstanceUsingThisAsync("foobar");

}

public static async Task ContinueWithStaticAsync(string str)
{
await Task.Delay(1000).ContinueWith(t =>
{
var code = t.Status;
var dt = new DateTime(4513, 4, 5, 6, 7, 8);
Console.WriteLine ($"First continueWith: {code}, {dt}"); //t, code, dt
});
Console.WriteLine ($"done with this method");
}

public static async Task NestedContinueWithStaticAsync(string str)
{
await Task.Delay(500).ContinueWith(async t =>
{
var code = t.Status;
var ncs_dt0 = new DateTime(3412, 4, 6, 8, 0, 2);
Console.WriteLine ($"First continueWith: {code}, {ncs_dt0}"); // t, code, str, dt0
await Task.Delay(300).ContinueWith(t2 =>
{
var ncs_dt1 = new DateTime(4513, 4, 5, 6, 7, 8);
Console.WriteLine ($"t2: {t2.Status}, str: {str}, {ncs_dt1}, {ncs_dt0}");//t2, dt1, str, dt0
});
});
Console.WriteLine ($"done with this method");
}

public async Task ContinueWithInstanceAsync(string str)
{
await Task.Delay(1000).ContinueWith(t =>
{
var code = t.Status;
var dt = new DateTime(4513, 4, 5, 6, 7, 8);
Console.WriteLine ($"First continueWith: {code}, {dt}");// t, code, dt
});
Console.WriteLine ($"done with this method");
}

public async Task ContinueWithInstanceUsingThisAsync(string str)
{
await Task.Delay(1000).ContinueWith(t =>
{
var code = t.Status;
var dt = new DateTime(4513, 4, 5, 6, 7, 8);
Console.WriteLine ($"First continueWith: {code}, {dt}, {this.Date}");
});
Console.WriteLine ($"done with this method");
}

[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public async Task NestedContinueWithInstanceAsync(string str)
{
await Task.Delay(500).ContinueWith(async t =>
{
var code = t.Status;
var dt0 = new DateTime(3412, 4, 6, 8, 0, 2);
if (str == "oi")
{
dt0 = new DateTime(3415, 4, 6, 8, 0, 2);
}
Console.WriteLine ($"First continueWith: {code}, {dt0}, {Date}");//this, t, code, str, dt0
await Task.Delay(300).ContinueWith(t2 =>
{
var dt1 = new DateTime(4513, 4, 5, 6, 7, 8);
Console.WriteLine ($"t2: {t2.Status}, str: {str}, {dt1}, {dt0}");//this, t2, dt1, str, dt0
});
});
Console.WriteLine ($"done with this method");
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public static async System.Threading.Tasks.Task RunInspectTask()
{
await getJsonTask.ContinueWith(t =>
{
int a = 10;
Console.WriteLine(a);
if (t.IsCompletedSuccessfully)
forecasts = t.Result;

Expand Down