diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs b/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs index 8d62ab3c5b9522..03783a7466e8da 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs @@ -9,6 +9,7 @@ using BrowserDebugProxy; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; +using System.Reflection; namespace Microsoft.WebAssembly.Diagnostics; @@ -269,18 +270,19 @@ async Task GetNullObjectClassName() private async Task 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) { @@ -293,6 +295,12 @@ private async Task 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(value: null, type: "object", description: description, className: className, objectId: $"dotnet:object:{objectId}"); } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs index 92afb5b2d28fba..8cd27c6e27b938 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs @@ -15,6 +15,7 @@ using System.Net.WebSockets; using BrowserDebugProxy; using System.Globalization; +using System.Reflection; namespace Microsoft.WebAssembly.Diagnostics { @@ -458,7 +459,7 @@ public async Task 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()}' cannot be indexed."); @@ -560,7 +561,7 @@ public async Task 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 @@ -666,7 +667,7 @@ async Task FindMethodIdOnLinqEnumerable(IList 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; diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index 902d339da852d4..074b7c8fc7df93 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -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); @@ -1737,7 +1737,7 @@ public async Task GetTypeIdFromToken(int assemblyId, int typeToken, Cancell return retDebuggerCmdReader.ReadInt32(); } - public async Task GetMethodIdsByName(int type_id, string method_name, CancellationToken token) + public async Task 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}"); @@ -1745,7 +1745,7 @@ public async Task GetMethodIdsByName(int type_id, string method_name, Can 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(); @@ -1839,6 +1839,38 @@ public Task InvokeMethod(DotnetObjectId dotnetObjectId, CancellationTok : throw new ArgumentException($"Cannot invoke method with id {methodId} on {dotnetObjectId}", nameof(dotnetObjectId)); } + public async Task InvokeToStringAsync(IEnumerable 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) + continue; + var retMethod = await InvokeMethod(objectId, methodId, isValueType, token); + return retMethod["value"]?["value"].Value(); + } + } + } + catch (Exception e) + { + logger.LogDebug($"Error while evaluating ToString method: {e}"); + } + return null; + } + public async Task GetPropertyMethodIdByName(int typeId, string propertyName, CancellationToken token) { using var retDebuggerCmdReader = await GetTypePropertiesReader(typeId, token); @@ -2169,7 +2201,7 @@ private async Task 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; diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs b/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs index 865785bf51e862..bcd34e9b7d0cc7 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.WebAssembly.Diagnostics; using Newtonsoft.Json.Linq; +using Microsoft.Extensions.Logging; namespace BrowserDebugProxy { @@ -116,11 +117,11 @@ public async Task 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(); + 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 } @@ -128,7 +129,15 @@ public async Task ToJObject(MonoSDBHelper sdbAgent, bool forDebuggerDis { 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, diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs index 1617a37053b10f..48f4f0a70804c1 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs @@ -106,5 +106,37 @@ async Task 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(); + 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")) + ); + } + ); + } } } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs index d0f8be8e660d85..a22b82a5d8d880 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs @@ -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]"), 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 5e30c999b3304d..325fc7d76ad859 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs @@ -1371,4 +1371,138 @@ public static void CheckArguments(ReadOnlySpan 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(); + } } \ No newline at end of file