Skip to content

[wasm][debugger] Fix debugger behavior when the type has ToString method overridden #76780

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

Merged
merged 11 commits into from
Oct 11, 2022
Merged
18 changes: 13 additions & 5 deletions src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using BrowserDebugProxy;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using System.Reflection;

namespace Microsoft.WebAssembly.Diagnostics;

Expand Down Expand Up @@ -269,18 +270,19 @@ async Task<string> GetNullObjectClassName()
private async Task<JObject> ReadAsObjectValue(MonoBinaryReader retDebuggerCmdReader, int typeIdFromAttribute, bool forDebuggerDisplayAttribute, CancellationToken token)
{
var objectId = retDebuggerCmdReader.ReadInt32();
var type_id = await _sdbAgent.GetTypeIdsForObject(objectId, false, token);
string className = await _sdbAgent.GetTypeName(type_id[0], token);
var typeIds = await _sdbAgent.GetTypeIdsForObject(objectId, withParents: true, token);
string className = await _sdbAgent.GetTypeName(typeIds[0], token);
string debuggerDisplayAttribute = null;
if (!forDebuggerDisplayAttribute)
debuggerDisplayAttribute = await _sdbAgent.GetValueFromDebuggerDisplayAttribute(
new DotnetObjectId("object", objectId), type_id[0], token);
new DotnetObjectId("object", objectId), typeIds[0], token);
var description = className.ToString();

if (debuggerDisplayAttribute != null)
{
description = debuggerDisplayAttribute;

if (await _sdbAgent.IsDelegate(objectId, token))
}
else if (await _sdbAgent.IsDelegate(objectId, token))
{
if (typeIdFromAttribute != -1)
{
Expand All @@ -293,6 +295,12 @@ private async Task<JObject> ReadAsObjectValue(MonoBinaryReader retDebuggerCmdRea
return Create(value: className, type: "symbol", description: className);
}
}
else
{
var toString = await _sdbAgent.InvokeToStringAsync(typeIds, isValueType: false, isEnum: false, objectId, BindingFlags.DeclaredOnly, token);
if (toString != null)
description = toString;
}
return Create<object>(value: null, type: "object", description: description, className: className, objectId: $"dotnet:object:{objectId}");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System.Net.WebSockets;
using BrowserDebugProxy;
using System.Globalization;
using System.Reflection;

namespace Microsoft.WebAssembly.Diagnostics
{
Expand Down Expand Up @@ -458,7 +459,7 @@ public async Task<JObject> Resolve(ElementAccessExpressionSyntax elementAccess,
return await ExpressionEvaluator.EvaluateSimpleExpression(this, eaFormatted, elementAccessStr, variableDefinitions, logger, token);
}
var typeIds = await context.SdbAgent.GetTypeIdsForObject(objectId.Value, true, token);
int[] methodIds = await context.SdbAgent.GetMethodIdsByName(typeIds[0], "ToArray", token);
int[] methodIds = await context.SdbAgent.GetMethodIdsByName(typeIds[0], "ToArray", BindingFlags.Default, token);
// ToArray should not have an overload, but if user defined it, take the default one: without params
if (methodIds == null)
throw new InvalidOperationException($"Type '{rootObject?["className"]?.Value<string>()}' cannot be indexed.");
Expand Down Expand Up @@ -560,7 +561,7 @@ public async Task<JObject> Resolve(InvocationExpressionSyntax method, Dictionary
{
typeIds = await context.SdbAgent.GetTypeIdsForObject(objectId.Value, true, token);
}
int[] methodIds = await context.SdbAgent.GetMethodIdsByName(typeIds[0], methodName, token);
int[] methodIds = await context.SdbAgent.GetMethodIdsByName(typeIds[0], methodName, BindingFlags.Default, token);
if (methodIds == null)
{
//try to search on System.Linq.Enumerable
Expand Down Expand Up @@ -666,7 +667,7 @@ async Task<int> FindMethodIdOnLinqEnumerable(IList<int> typeIds, string methodNa
}
}

int[] newMethodIds = await context.SdbAgent.GetMethodIdsByName(linqTypeId, methodName, token);
int[] newMethodIds = await context.SdbAgent.GetMethodIdsByName(linqTypeId, methodName, BindingFlags.Default, token);
if (newMethodIds == null)
return 0;

Expand Down
40 changes: 36 additions & 4 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ internal sealed class MonoSDBHelper
private DebugStore store;
private SessionId sessionId;

private readonly ILogger logger;
internal readonly ILogger logger;
private static readonly Regex regexForAsyncLocals = new (@"\<([^)]*)\>", RegexOptions.Singleline);
private static readonly Regex regexForAsyncMethodName = new (@"\<([^>]*)\>([d][_][_])([0-9]*)", RegexOptions.Compiled);
private static readonly Regex regexForGenericArgs = new (@"[`][0-9]+", RegexOptions.Compiled);
Expand Down Expand Up @@ -1737,15 +1737,15 @@ public async Task<int> GetTypeIdFromToken(int assemblyId, int typeToken, Cancell
return retDebuggerCmdReader.ReadInt32();
}

public async Task<int[]> GetMethodIdsByName(int type_id, string method_name, CancellationToken token)
public async Task<int[]> GetMethodIdsByName(int type_id, string method_name, BindingFlags extraFlags, CancellationToken token)
{
if (type_id <= 0)
throw new DebuggerAgentException($"Invalid type_id {type_id} (method_name: {method_name}");

using var commandParamsWriter = new MonoBinaryWriter();
commandParamsWriter.Write((int)type_id);
commandParamsWriter.Write(method_name);
commandParamsWriter.Write((int)(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static));
commandParamsWriter.Write((int)(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | extraFlags));
commandParamsWriter.Write((int)1); //case sensitive
using var retDebuggerCmdReader = await SendDebuggerAgentCommand(CmdType.GetMethodsByNameFlags, commandParamsWriter, token);
var nMethods = retDebuggerCmdReader.ReadInt32();
Expand Down Expand Up @@ -1839,6 +1839,38 @@ public Task<JObject> InvokeMethod(DotnetObjectId dotnetObjectId, CancellationTok
: throw new ArgumentException($"Cannot invoke method with id {methodId} on {dotnetObjectId}", nameof(dotnetObjectId));
}

public async Task<string> InvokeToStringAsync(IEnumerable<int> typeIds, bool isValueType, bool isEnum, int objectId, BindingFlags extraFlags, CancellationToken token)
{
try
{
foreach (var typeId in typeIds)
{
var typeInfo = await GetTypeInfo(typeId, token);
if (typeInfo == null || typeInfo.Name == "object")
continue;
Microsoft.WebAssembly.Diagnostics.MethodInfo methodInfo = typeInfo.Info.Methods.FirstOrDefault(m => m.Name == "ToString");
if (isEnum != true && methodInfo == null)
continue;
int[] methodIds = await GetMethodIdsByName(typeId, "ToString", extraFlags, token);
if (methodIds == null)
continue;
foreach (var methodId in methodIds)
{
var methodInfoFromRuntime = await GetMethodInfo(methodId, token);
if (methodInfoFromRuntime.Info.GetParametersInfo().Length > 0)
Copy link
Member

Choose a reason for hiding this comment

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

MethodInfoWithDebugInformation implements GetParametersInfo(), so we could shorten:

Suggested change
if (methodInfoFromRuntime.Info.GetParametersInfo().Length > 0)
if (methodInfoFromRuntime.GetParametersInfo().Length > 0)

continue;
var retMethod = await InvokeMethod(objectId, methodId, isValueType, token);
return retMethod["value"]?["value"].Value<string>();
}
}
}
catch (Exception e)
{
logger.LogDebug($"Error while evaluating ToString method: {e}");
}
return null;
}

public async Task<int> GetPropertyMethodIdByName(int typeId, string propertyName, CancellationToken token)
{
using var retDebuggerCmdReader = await GetTypePropertiesReader(typeId, token);
Expand Down Expand Up @@ -2169,7 +2201,7 @@ private async Task<int> FindDebuggerProxyConstructorIdFor(int typeId, Cancellati
break;
cAttrTypeId = genericTypeId;
}
int[] methodIds = await GetMethodIdsByName(cAttrTypeId, ".ctor", token);
int[] methodIds = await GetMethodIdsByName(cAttrTypeId, ".ctor", BindingFlags.Default, token);
if (methodIds != null)
methodId = methodIds[0];
break;
Expand Down
19 changes: 14 additions & 5 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Threading.Tasks;
using Microsoft.WebAssembly.Diagnostics;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;

namespace BrowserDebugProxy
{
Expand Down Expand Up @@ -116,19 +117,27 @@ public async Task<JObject> ToJObject(MonoSDBHelper sdbAgent, bool forDebuggerDis
string description = className;
if (ShouldAutoInvokeToString(className) || IsEnum)
{
int[] methodIds = await sdbAgent.GetMethodIdsByName(TypeId, "ToString", token);
if (methodIds == null)
throw new InternalErrorException($"Cannot find method 'ToString' on typeId = {TypeId}");
var retMethod = await sdbAgent.InvokeMethod(Buffer, methodIds[0], token, "methodRet");
description = retMethod["value"]?["value"].Value<string>();
var toString = await sdbAgent.InvokeToStringAsync(new int[]{ TypeId }, isValueType: true, IsEnum, Id.Value, IsEnum ? BindingFlags.Default : BindingFlags.DeclaredOnly, token);
if (toString == null)
sdbAgent.logger.LogDebug($"Error while evaluating ToString method on typeId = {TypeId}");
else
description = toString;
if (className.Equals("System.Guid"))
description = description.ToUpperInvariant(); //to keep the old behavior
}
else if (!forDebuggerDisplayAttribute)
{
string displayString = await sdbAgent.GetValueFromDebuggerDisplayAttribute(Id, TypeId, token);
if (displayString != null)
{
description = displayString;
}
else
{
var toString = await sdbAgent.InvokeToStringAsync(new int[]{ TypeId }, isValueType: true, IsEnum, Id.Value, IsEnum ? BindingFlags.Default : BindingFlags.DeclaredOnly, token);
if (toString != null)
description = toString;
}
}
return JObjectValueCreator.Create(
IsEnum ? fields[0]["value"] : null,
Expand Down
32 changes: 32 additions & 0 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,37 @@ async Task<bool> CheckProperties(JObject pause_location)
Assert.True(task.Result);
}
}

[ConditionalFact(nameof(RunningOnChrome))]
public async Task InspectObjectOfTypeWithToStringOverriden()
{
var expression = $"{{ invoke_static_method('[debugger-test] ToStringOverriden:Run'); }}";

await EvaluateAndCheck(
"window.setTimeout(function() {" + expression + "; }, 1);",
"dotnet://debugger-test.dll/debugger-test.cs", 1505, 8,
"ToStringOverriden.Run",
wait_for_event_fn: async (pause_location) =>
{
var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
await EvaluateOnCallFrameAndCheck(id,
("a", TObject("ToStringOverriden", description:"helloToStringOverriden")),
("b", TObject("ToStringOverriden.ToStringOverridenB", description:"helloToStringOverridenA")),
("c", TObject("ToStringOverriden.ToStringOverridenD", description:"helloToStringOverridenD")),
("d", TObject("ToStringOverriden.ToStringOverridenE", description:"helloToStringOverridenE")),
("e", TObject("ToStringOverriden.ToStringOverridenB", description:"helloToStringOverridenA")),
("f", TObject("ToStringOverriden.ToStringOverridenB", description:"helloToStringOverridenA")),
("g", TObject("ToStringOverriden.ToStringOverridenG", description:"helloToStringOverridenG")),
("h", TObject("ToStringOverriden.ToStringOverridenH", description:"helloToStringOverridenH")),
("i", TObject("ToStringOverriden.ToStringOverridenI", description:"ToStringOverriden.ToStringOverridenI")),
("j", TObject("ToStringOverriden.ToStringOverridenJ", description:"helloToStringOverridenJ")),
("k", TObject("ToStringOverriden.ToStringOverridenK", description:"ToStringOverriden.ToStringOverridenK")),
("l", TObject("ToStringOverriden.ToStringOverridenL", description:"helloToStringOverridenL")),
("m", TObject("ToStringOverriden.ToStringOverridenM", description:"ToStringOverridenM { }")),
("n", TObject("ToStringOverriden.ToStringOverridenN", description:"helloToStringOverridenN"))
);
}
);
}
}
}
2 changes: 1 addition & 1 deletion src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ public async Task PreviousFrameForAReflectedCall() => await CheckInspectLocalsAt

await CheckProps(frame_locals, new
{
mi = TObject("System.Reflection.RuntimeMethodInfo"), //this is what is returned when debugging desktop apps using VS
mi = TObject("System.Reflection.RuntimeMethodInfo", description: "Void SimpleStaticMethod(System.DateTime, System.String)"), //this is what is returned when debugging desktop apps using VS
dt = TDateTime(new DateTime(4210, 3, 4, 5, 6, 7)),
i = TNumber(4),
strings = TArray("string[]", "string[1]"),
Expand Down
134 changes: 134 additions & 0 deletions src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1371,4 +1371,138 @@ public static void CheckArguments(ReadOnlySpan<object> parameters)
{
System.Diagnostics.Debugger.Break();
}
}

public class ToStringOverriden
{
class ToStringOverridenA {
public override string ToString()
{
return "helloToStringOverridenA";
}
}
class ToStringOverridenB: ToStringOverridenA {}

class ToStringOverridenC {}
class ToStringOverridenD: ToStringOverridenC
{
public override string ToString()
{
return "helloToStringOverridenD";
}
}

struct ToStringOverridenE
{
public override string ToString()
{
return "helloToStringOverridenE";
}
}

class ToStringOverridenF
{
public override string ToString()
{
return "helloToStringOverridenF";
}
}
class ToStringOverridenG: ToStringOverridenF
{
public override string ToString()
{
return "helloToStringOverridenG";
}
}

class ToStringOverridenH
{
public override string ToString()
{
return "helloToStringOverridenH";
}
public string ToString(bool withParms = true)
{
return "helloToStringOverridenHWrong";
}
}

class ToStringOverridenI
{
public string ToString(bool withParms = true)
{
return "helloToStringOverridenIWrong";
}
}

struct ToStringOverridenJ
{
public override string ToString()
{
return "helloToStringOverridenJ";
}
public string ToString(bool withParms = true)
{
return "helloToStringOverridenJWrong";
}
}

struct ToStringOverridenK
{
public string ToString(bool withParms = true)
{
return "helloToStringOverridenKWrong";
}
}

record ToStringOverridenL
{
public override string ToString()
{
return "helloToStringOverridenL";
}
}

record ToStringOverridenM
{
public string ToString(bool withParms = true)
{
return "helloToStringOverridenMWrong";
}
}

record ToStringOverridenN
{
public override string ToString()
{
return "helloToStringOverridenN";
}
public string ToString(bool withParms = true)
{
return "helloToStringOverridenNWrong";
}
}

public override string ToString()
{
return "helloToStringOverriden";
}
public static void Run()
{
var a = new ToStringOverriden();
var b = new ToStringOverridenB();
var c = new ToStringOverridenD();
var d = new ToStringOverridenE();
ToStringOverridenA e = new ToStringOverridenB();
object f = new ToStringOverridenB();
var g = new ToStringOverridenG();
var h = new ToStringOverridenH();
var i = new ToStringOverridenI();
var j = new ToStringOverridenJ();
var k = new ToStringOverridenK();
var l = new ToStringOverridenL();
var m = new ToStringOverridenM();
var n = new ToStringOverridenN();
System.Diagnostics.Debugger.Break();
}
}