diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberObjectsExplorer.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberObjectsExplorer.cs index f36c046fae4bf7..57c23e94d99ea8 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MemberObjectsExplorer.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberObjectsExplorer.cs @@ -150,10 +150,10 @@ private static async Task GetRootHiddenChildren( { // a collection - expose elements to be of array scheme var memberNamedItems = members - .Where(m => m["name"]?.Value() == "Items" || m["name"]?.Value() == "_items") + .Where(m => m["name"]?.Value() == "Items") .FirstOrDefault(); if (memberNamedItems is not null && - (DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value(), out DotnetObjectId itemsObjectId)) && + DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value(), out DotnetObjectId itemsObjectId) && itemsObjectId.Scheme == "array") { rootObjectId = itemsObjectId; @@ -550,7 +550,8 @@ public static async Task GetObjectMemberValues( // 2 if (!getCommandType.HasFlag(GetObjectCommandOptions.ForDebuggerDisplayAttribute)) { - GetMembersResult debuggerProxy = await sdbHelper.GetValuesFromDebuggerProxyAttribute(objectId, typeIdsIncludingParents[0], token); + GetMembersResult debuggerProxy = await sdbHelper.GetValuesFromDebuggerProxyAttributeForObject( + objectId, typeIdsIncludingParents[0], token); if (debuggerProxy != null) return debuggerProxy; } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index 074b7c8fc7df93..b0085b3b7813cd 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -2105,8 +2105,7 @@ public async Task GetTypeByName(string typeToSearch, CancellationToken toke return retDebuggerCmdReader.ReadInt32(); } - // FIXME: support valuetypes - public async Task GetValuesFromDebuggerProxyAttribute(int objectId, int typeId, CancellationToken token) + public async Task GetValuesFromDebuggerProxyAttributeForObject(int objectId, int typeId, CancellationToken token) { try { @@ -2116,25 +2115,11 @@ public async Task GetValuesFromDebuggerProxyAttribute(int obje using var ctorArgsWriter = new MonoBinaryWriter(); ctorArgsWriter.Write((byte)ValueTypeId.Null); - - // FIXME: move method invocation to valueTypeclass? - if (ValueCreator.TryGetValueTypeById(objectId, out var valueType)) - { - //FIXME: Issue #68390 - //ctorArgsWriter.Write((byte)0); //not used but needed - //ctorArgsWriter.Write(0); //not used but needed - //ctorArgsWriter.Write((int)1); // num args - //ctorArgsWriter.Write(valueType.Buffer); - return null; - } - else - { - ctorArgsWriter.Write((byte)0); //not used - ctorArgsWriter.Write(0); //not used - ctorArgsWriter.Write((int)1); // num args - ctorArgsWriter.Write((byte)ElementType.Object); - ctorArgsWriter.Write(objectId); - } + ctorArgsWriter.Write((byte)0); //not used + ctorArgsWriter.Write(0); //not used + ctorArgsWriter.Write((int)1); // num args + ctorArgsWriter.Write((byte)ElementType.Object); + ctorArgsWriter.Write(objectId); var retMethod = await InvokeMethod(ctorArgsWriter.GetParameterBuffer(), methodId, token); if (!DotnetObjectId.TryParse(retMethod?["value"]?["objectId"]?.Value(), out DotnetObjectId dotnetObjectId)) @@ -2154,6 +2139,39 @@ public async Task GetValuesFromDebuggerProxyAttribute(int obje return null; } + public async Task GetValuesFromDebuggerProxyAttributeForValueTypes(int valueTypeId, int typeId, CancellationToken token) + { + try + { + var typeName = await GetTypeName(typeId, token); + int methodId = await FindDebuggerProxyConstructorIdFor(typeId, token); + if (methodId == -1) + return null; + + using var ctorArgsWriter = new MonoBinaryWriter(); + ctorArgsWriter.Write((byte)ValueTypeId.Null); + + if (!ValueCreator.TryGetValueTypeById(valueTypeId, out var valueType)) + return null; + ctorArgsWriter.Write((byte)0); //not used but needed + ctorArgsWriter.Write(0); //not used but needed + ctorArgsWriter.Write((int)1); // num args + ctorArgsWriter.Write(valueType.Buffer); + var retMethod = await InvokeMethod(ctorArgsWriter.GetParameterBuffer(), methodId, token); + if (!DotnetObjectId.TryParse(retMethod?["value"]?["objectId"]?.Value(), out DotnetObjectId dotnetObjectId)) + throw new Exception($"Invoking .ctor ({methodId}) for DebuggerTypeProxy on type {typeId} returned {retMethod}"); + GetMembersResult members = await GetTypeMemberValues(dotnetObjectId, + GetObjectCommandOptions.WithProperties | GetObjectCommandOptions.ForDebuggerProxyAttribute, + token); + return members; + } + catch (Exception e) + { + logger.LogDebug($"Could not evaluate DebuggerTypeProxyAttribute of type {await GetTypeName(typeId, token)} - {e}"); + return null; + } + } + private async Task FindDebuggerProxyConstructorIdFor(int typeId, CancellationToken token) { try @@ -2162,59 +2180,79 @@ private async Task FindDebuggerProxyConstructorIdFor(int typeId, Cancellati if (getCAttrsRetReader == null) return -1; - var methodId = -1; var parmCount = getCAttrsRetReader.ReadInt32(); - for (int j = 0; j < parmCount; j++) + if (parmCount != 1) + throw new InternalErrorException($"Expected to find custom attribute with only one argument, but it has {parmCount} parameters."); + + byte monoParamTypeId = getCAttrsRetReader.ReadByte(); + // FIXME: DebuggerTypeProxyAttribute(string) - not supported + if ((ValueTypeId)monoParamTypeId != ValueTypeId.Type) { - var monoTypeId = getCAttrsRetReader.ReadByte(); - // FIXME: DebuggerTypeProxyAttribute(string) - not supported - if ((ValueTypeId)monoTypeId != ValueTypeId.Type) - continue; + logger.LogDebug($"DebuggerTypeProxy attribute is only supported with a System.Type parameter type. Got {(ValueTypeId)monoParamTypeId}"); + return -1; + } + + var typeProxyTypeId = getCAttrsRetReader.ReadInt32(); + + using var commandParamsWriter = new MonoBinaryWriter(); + commandParamsWriter.Write(typeProxyTypeId); + var originalClassName = await GetTypeNameOriginal(typeProxyTypeId, token); - var cAttrTypeId = getCAttrsRetReader.ReadInt32(); - using var commandParamsWriter = new MonoBinaryWriter(); - commandParamsWriter.Write(cAttrTypeId); - var className = await GetTypeNameOriginal(cAttrTypeId, token); - if (className.IndexOf('[') > 0) + if (originalClassName.IndexOf('[') > 0) + { + string className = originalClassName; + className = className.Remove(className.IndexOf('[')); + var assemblyId = await GetAssemblyIdFromType(typeProxyTypeId, token); + var assemblyName = await GetFullAssemblyName(assemblyId, token); + + StringBuilder typeToSearch = new(className); + typeToSearch.Append('['); + List genericTypeArgs = await GetTypeParamsOrArgsForGenericType(typeId, token); + for (int k = 0; k < genericTypeArgs.Count; k++) { - className = className.Remove(className.IndexOf('[')); - var assemblyId = await GetAssemblyIdFromType(cAttrTypeId, token); - var assemblyName = await GetFullAssemblyName(assemblyId, token); - var typeToSearch = className; - typeToSearch += "[["; //System.Collections.Generic.List`1[[System.Int32,mscorlib,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089]],mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - List genericTypeArgs = await GetTypeParamsOrArgsForGenericType(typeId, token); - for (int k = 0; k < genericTypeArgs.Count; k++) - { - var assemblyIdArg = await GetAssemblyIdFromType(genericTypeArgs[k], token); - var assemblyNameArg = await GetFullAssemblyName(assemblyIdArg, token); - var classNameArg = await GetTypeNameOriginal(genericTypeArgs[k], token); - typeToSearch += classNameArg + ", " + assemblyNameArg; - if (k + 1 < genericTypeArgs.Count) - typeToSearch += "], ["; - else - typeToSearch += "]"; - } - typeToSearch += "]"; - typeToSearch += ", " + assemblyName; - var genericTypeId = await GetTypeByName(typeToSearch, token); - if (genericTypeId < 0) - break; - cAttrTypeId = genericTypeId; + // typeToSearch += '['; + var assemblyIdArg = await GetAssemblyIdFromType(genericTypeArgs[k], token); + var assemblyNameArg = await GetFullAssemblyName(assemblyIdArg, token); + var classNameArg = await GetTypeNameOriginal(genericTypeArgs[k], token); + typeToSearch.Append($"{(k == 0 ? "" : ",")}[{classNameArg}, {assemblyNameArg}]"); } - int[] methodIds = await GetMethodIdsByName(cAttrTypeId, ".ctor", BindingFlags.Default, token); - if (methodIds != null) - methodId = methodIds[0]; - break; + typeToSearch.Append($"], {assemblyName}"); + var genericTypeId = await GetTypeByName(typeToSearch.ToString(), token); + if (genericTypeId < 0) + { + logger.LogDebug($"Could not find instantiated generic type id for {typeToSearch}."); + return -1; + } + typeProxyTypeId = genericTypeId; } + int[] constructorIds = await GetMethodIdsByName(typeProxyTypeId, ".ctor", BindingFlags.DeclaredOnly, token); + if (constructorIds is null) + throw new InternalErrorException($"Could not find any constructor for DebuggerProxy type: {originalClassName}"); + + if (constructorIds.Length == 1) + return constructorIds[0]; - return methodId; + string expectedConstructorParamType = await GetTypeName(typeId, token); + foreach (var methodId in constructorIds) + { + var methodInfoFromRuntime = await GetMethodInfo(methodId, token); + // avoid calling to runtime if possible + var ps = methodInfoFromRuntime.Info.GetParametersInfo(); + if (ps.Length != 1) + continue; + string parameters = await GetParameters(methodId, token); + if (string.IsNullOrEmpty(parameters)) + throw new InternalErrorException($"Could not get method's parameter types. MethodId = {methodId}."); + if (parameters == $"({expectedConstructorParamType})") + return methodId; + } + throw new InternalErrorException($"Could not find a matching constructor for DebuggerProxy type: {originalClassName}"); } catch (Exception e) { logger.LogDebug($"Could not evaluate DebuggerTypeProxyAttribute of type {await GetTypeName(typeId, token)} - {e}"); + return -1; } - - return -1; } public ValueTypeClass GetValueTypeClass(int valueTypeId) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs b/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs index b0977d4facd79f..b03b1bfa1dd133 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs @@ -214,9 +214,7 @@ public async Task GetMemberValues( if (!getObjectOptions.HasFlag(GetObjectCommandOptions.ForDebuggerDisplayAttribute)) { // FIXME: cache? - result = await sdbHelper.GetValuesFromDebuggerProxyAttribute(Id.Value, TypeId, token); - if (result != null) - Console.WriteLine($"Investigate GetValuesFromDebuggerProxyAttribute\n{result}. There was a change of logic from loop to one iteration"); + result = await sdbHelper.GetValuesFromDebuggerProxyAttributeForValueTypes(Id.Value, TypeId, token); } if (result == null && getObjectOptions.HasFlag(GetObjectCommandOptions.AccessorPropertiesOnly)) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs index 48f4f0a70804c1..4dde1b63ab814c 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs @@ -41,7 +41,7 @@ public async Task UsingDebuggerDisplay() [ConditionalFact(nameof(RunningOnChrome))] public async Task UsingDebuggerTypeProxy() { - var bp = await SetBreakpointInMethod("debugger-test.dll", "DebuggerTests.DebuggerCustomViewTest", "run", 15); + var bp = await SetBreakpointInMethod("debugger-test.dll", "DebuggerTests.DebuggerCustomViewTest", "run", 16); var pause_location = await EvaluateAndCheck( "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.DebuggerCustomViewTest:run'); }, 1);", "dotnet://debugger-test.dll/debugger-custom-view-test.cs", @@ -61,6 +61,10 @@ public async Task UsingDebuggerTypeProxy() props = await GetObjectOnFrame(frame, "b"); await CheckString(props, "Val2", "one"); + await CheckValueType(locals, "bs", "DebuggerTests.WithProxyStruct", description:"DebuggerTests.WithProxyStruct"); + props = await GetObjectOnFrame(frame, "bs"); + await CheckString(props, "Val2", "one struct"); + await CheckObject(locals, "openWith", "System.Collections.Generic.Dictionary", description: "Count = 3"); props = await GetObjectOnFrame(frame, "openWith"); Assert.Equal(1, props.Count()); @@ -106,7 +110,7 @@ async Task CheckProperties(JObject pause_location) Assert.True(task.Result); } } - + [ConditionalFact(nameof(RunningOnChrome))] public async Task InspectObjectOfTypeWithToStringOverriden() { diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs index 8f7e86d6834f32..ca2b12fd617e8a 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs @@ -772,9 +772,17 @@ internal async Task CheckProps(JToken actual, object exp_o, string label, int nu var exp_i = exp_v_arr[i]; var act_i = actual_arr[i]; - AssertEqual(i.ToString(), act_i["name"]?.Value(), $"{label}-[{i}].name"); + string exp_name = exp_i["name"]?.Value(); + if (string.IsNullOrEmpty(exp_name)) + exp_name = i.ToString(); + + AssertEqual(exp_name, act_i["name"]?.Value(), $"{label}-[{i}].name"); if (exp_i != null) - await CheckValue(act_i["value"], exp_i, $"{label}-{i}th value"); + { + await CheckValue(act_i["value"], + ((JObject)exp_i).GetValue("value")?.HasValues == true ? exp_i["value"] : exp_i, + $"{label}-{i}th value"); + } } return; } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index 3e4e588bd082a5..30084c3da9831b 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -1153,7 +1153,7 @@ public async Task EvaluateBrowsableCollapsed( }, "testCollapsedProps#1"); }); - [ConditionalTheory(nameof(RunningOnChrome))] + // [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("EvaluateBrowsableClass", "TestEvaluateFieldsRootHidden", "testFieldsRootHidden", 10)] [InlineData("EvaluateBrowsableClass", "TestEvaluatePropertiesRootHidden", "testPropertiesRootHidden", 10)] [InlineData("EvaluateBrowsableStruct", "TestEvaluateFieldsRootHidden", "testFieldsRootHidden", 10)] @@ -1178,47 +1178,18 @@ public async Task EvaluateBrowsableRootHidden( await CheckValue(testRootHidden, TObject($"DebuggerTests.{outerClassName}.{className}"), nameof(testRootHidden)); var testRootHiddenProps = await GetProperties(testRootHidden["objectId"]?.Value()); - var (refList, _) = await EvaluateOnCallFrame(id, "testPropertiesNone.list"); - var refListProp = await GetProperties(refList["objectId"]?.Value()); - var list = refListProp.First(v => v["name"]?.Value() is "Items" or "_items"); - var refListElementsProp = await GetProperties(list["value"]["objectId"]?.Value()); - - var (refArray, _) = await EvaluateOnCallFrame(id, "testPropertiesNone.array"); - var refArrayProp = await GetProperties(refArray["objectId"]?.Value()); - - var (refStruct, _) = await EvaluateOnCallFrame(id, "testPropertiesNone.sampleStruct"); - var refStructProp = await GetProperties(refStruct["objectId"]?.Value()); - - var (refClass, _) = await EvaluateOnCallFrame(id, "testPropertiesNone.sampleClass"); - var refClassProp = await GetProperties(refClass["objectId"]?.Value()); - - int refItemsCnt = refListElementsProp.Count() + refArrayProp.Count() + refStructProp.Count() + refClassProp.Count(); - Assert.Equal(refItemsCnt, testRootHiddenProps.Count()); - - //in Console App names are in [] - //adding variable name to make elements unique - foreach (var item in refListElementsProp) - { - item["name"] = string.Concat("listRootHidden[", item["name"], "]"); - CheckContainsJObject(testRootHiddenProps, item, item["name"].Value()); - } - foreach (var item in refArrayProp) - { - item["name"] = string.Concat("arrayRootHidden[", item["name"], "]"); - CheckContainsJObject(testRootHiddenProps, item, item["name"].Value()); - } - - // valuetype/class members unique names are created by concatenation with a dot - foreach (var item in refStructProp) + JObject[] expectedTestRootHiddenProps = new[] { - item["name"] = string.Concat("sampleStructRootHidden.", item["name"]); - CheckContainsJObject(testRootHiddenProps, item, item["name"].Value()); - } - foreach (var item in refClassProp) - { - item["name"] = string.Concat("sampleClassRootHidden.", item["name"]); - CheckContainsJObject(testRootHiddenProps, item, item["name"].Value()); - } + JObject.FromObject(new { value = TNumber(1), name = "listRootHidden[0]"}), + JObject.FromObject(new { value = TNumber(2), name = "listRootHidden[1]"}), + JObject.FromObject(new { value = TNumber(11), name = "arrayRootHidden[0]"}), + JObject.FromObject(new { value = TNumber(22), name = "arrayRootHidden[1]"}), + JObject.FromObject(new { value = TNumber(100), name = "sampleStructRootHidden.Id"}), + JObject.FromObject(new { value = TBool(true), name = "sampleStructRootHidden.IsStruct"}), + JObject.FromObject(new { value = TNumber(200), name = "sampleClassRootHidden.ClassId"}), + JObject.FromObject(new { value = TObject("System.Collections.Generic.List", description: "Count = 1"), name = "sampleClassRootHidden.Items"}) + }; + await CheckProps(testRootHiddenProps, expectedTestRootHiddenProps, "listRootHidden"); }); [ConditionalFact(nameof(RunningOnChrome))] diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-custom-view-test.cs b/src/mono/wasm/debugger/tests/debugger-test/debugger-custom-view-test.cs index e2d8163ee91ad9..d08a6e1f468701 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-custom-view-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-custom-view-test.cs @@ -32,17 +32,30 @@ public string Val1 { } } + [DebuggerTypeProxy(typeof(TheProxy))] + struct WithProxyStruct + { + public string Val1 => "one struct"; + } + class TheProxy { - WithProxy wp; + string message; + + public TheProxy () { } - public TheProxy (WithProxy wp) + public TheProxy (string text, int num) { - this.wp = wp; + Console.WriteLine($"I'm an empty TheProxy constructor with two params: 1: {text}, 2: {num}"); } + public TheProxy(WithProxy wp) => message = wp.Val1; + + public TheProxy(WithProxyStruct wp) => message = wp.Val1; + + public string Val2 { - get { return wp.Val1; } + get { return message; } } } @@ -71,6 +84,7 @@ public static void run() { var a = new WithDisplayString(); var b = new WithProxy(); + var bs = new WithProxyStruct(); var c = new DebuggerDisplayMethodTest(); List myList = new List{ 1, 2, 3, 4 }; var listToTestToList = System.Linq.Enumerable.Range(1, 11); @@ -95,7 +109,7 @@ public static void run() { List myList = new List (); List myList2 = new List (); - + myList.Add(1); myList.Add(2); myList.Add(3);