Skip to content
Original file line numberDiff line numberDiff line change
Expand Up@@ -150,10 +150,10 @@ private static async Task<JArray> GetRootHiddenChildren(
{
// a collection - expose elements to be of array scheme
var memberNamedItems = members
.Where(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items")
.Where(m => m["name"]?.Value<string>() == "Items")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what cases(example code) would we be executing this block?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

All cases in DebuggerTests.EvaluateOnCallFrameTests.EvaluateBrowsableRootHidden because

Error Message:
[] Could not find variable 'listRootHidden[0]'

This PR fixes only the problem with Items vs _items (now it's always Items). But it does not fix the fact that they need to get expanded manually - rootHidden info does not exist on them and cannot be easily extracted. The issue where we address it is this one: #76876.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is my understanding here correct?:

  1. List<T> has [DebuggerTypeProxy(typeof(ICollectionDebugView<>))], so we look at the proxy
  2. the proxy has Items property
  3. Items has [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)], which isn't working yet ([wasm][debugger] RootHidden is not working correctly in DebuggerTypeProxy #76876)
  4. So, we are expanding Items here to work specifically for List<T>?

If so, then is this only to get EvaluateBrowsableRootHidden passing?
Also, does #76876 still happen after the DebuggerTypeProxy fix here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack.
And in a follow up PR, the root hidden issue can be fixed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would say it's a misunderstanding caused by me not deleting the dependency on non-fixed values in the tests properly. Manual expansion was not done on an object decorated with RootHidden but on testPropertiesNone. The else block is not to make the tests pass, it is necessary till RootHidden on Items will be received correctly by Proxy. It was the test that was not 100% logical.
I think now it will make more sense after the last commit.

Yes, the issue is still here. Proxy is not receiving any DebuggerAttributes assigned to Items, so it does not know it's of rootHidden type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack. And in a follow up PR, the root hidden issue can be fixed.

Sorry, this comment did not display for me when I was investigating. I don't agree. This will break mechanism for all Collections that are in rootHidden objects. The else block can be removed when we have the fix already, so that we won't be changing the behavior from working to not working.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

else
{
// a collection - expose elements to be of array scheme
varmemberNamedItems=members
.Where(m =>m["name"]?.Value<string>()=="Items"||m["name"]?.Value<string>()=="_items")
.FirstOrDefault();
if(memberNamedItemsis not null&&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(),outDotnetObjectIditemsObjectId))&&
itemsObjectId.Scheme=="array")
{
rootObjectId=itemsObjectId;
}
}

This code is very explicitly looking for a Items member which is an array. So, the else arm is affecting only the very specific types that have such a member, which is the case for List<T>. The proxy, or the debugger agent aren't populating it as an extra thing.

I agree that we shouldn't break the behavior in this PR. But it is incorrect. We want to avoid depending on magic like that, especially when proper mechanisms are available to get the information - DebuggerTypeProxy in this case.

The rootHidden issue is in addition to this, and that's fine to fix separately.

.FirstOrDefault();
if (memberNamedItems is not null &&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId)) &&
DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId) &&
itemsObjectId.Scheme == "array")
{
rootObjectId = itemsObjectId;
Expand DownExpand Up@@ -550,7 +550,8 @@ public static async Task<GetMembersResult> 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;
}
Expand Down
162 changes: 100 additions & 62 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2105,8 +2105,7 @@ public async Task<int> GetTypeByName(string typeToSearch, CancellationToken toke
return retDebuggerCmdReader.ReadInt32();
}

// FIXME: support valuetypes
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int objectId, int typeId, CancellationToken token)
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttributeForObject(int objectId, int typeId, CancellationToken token)
{
try
{
Expand All@@ -2116,25 +2115,11 @@ public async Task<GetMembersResult> 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<string>(), out DotnetObjectId dotnetObjectId))
Expand All@@ -2154,6 +2139,39 @@ public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int obje
return null;
}

public async Task<GetMembersResult> 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<string>(), 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<int> FindDebuggerProxyConstructorIdFor(int typeId, CancellationToken token)
{
try
Expand All@@ -2162,59 +2180,79 @@ private async Task<int> 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<int> 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<int> 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)
Expand Down
4 changes: 1 addition & 3 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,9 +214,7 @@ public async Task<GetMembersResult> 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))
Expand Down
8 changes: 6 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand All@@ -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<string, string>", description: "Count = 3");
props = await GetObjectOnFrame(frame, "openWith");
Assert.Equal(1, props.Count());
Expand DownExpand Up@@ -106,7 +110,7 @@ async Task<bool> CheckProperties(JObject pause_location)
Assert.True(task.Result);
}
}

[ConditionalFact(nameof(RunningOnChrome))]
public async Task InspectObjectOfTypeWithToStringOverriden()
{
Expand Down
12 changes: 10 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>(), $"{label}-[{i}].name");
string exp_name = exp_i["name"]?.Value<string>();
if (string.IsNullOrEmpty(exp_name))
exp_name = i.ToString();

AssertEqual(exp_name, act_i["name"]?.Value<string>(), $"{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;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[wasm][debugger] Fix DebuggerTypeProxy for structs and for classes with multiple constructors by ilonatommy · Pull Request #77039 · dotnet/runtime · GitHub
Skip to content
Original file line numberDiff line numberDiff line change
Expand Up@@ -150,10 +150,10 @@ private static async Task<JArray> GetRootHiddenChildren(
{
// a collection - expose elements to be of array scheme
var memberNamedItems = members
.Where(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items")
.Where(m => m["name"]?.Value<string>() == "Items")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what cases(example code) would we be executing this block?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

All cases in DebuggerTests.EvaluateOnCallFrameTests.EvaluateBrowsableRootHidden because

Error Message:
[] Could not find variable 'listRootHidden[0]'

This PR fixes only the problem with Items vs _items (now it's always Items). But it does not fix the fact that they need to get expanded manually - rootHidden info does not exist on them and cannot be easily extracted. The issue where we address it is this one: #76876.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is my understanding here correct?:

  1. List<T> has [DebuggerTypeProxy(typeof(ICollectionDebugView<>))], so we look at the proxy
  2. the proxy has Items property
  3. Items has [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)], which isn't working yet ([wasm][debugger] RootHidden is not working correctly in DebuggerTypeProxy #76876)
  4. So, we are expanding Items here to work specifically for List<T>?

If so, then is this only to get EvaluateBrowsableRootHidden passing?
Also, does #76876 still happen after the DebuggerTypeProxy fix here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack.
And in a follow up PR, the root hidden issue can be fixed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would say it's a misunderstanding caused by me not deleting the dependency on non-fixed values in the tests properly. Manual expansion was not done on an object decorated with RootHidden but on testPropertiesNone. The else block is not to make the tests pass, it is necessary till RootHidden on Items will be received correctly by Proxy. It was the test that was not 100% logical.
I think now it will make more sense after the last commit.

Yes, the issue is still here. Proxy is not receiving any DebuggerAttributes assigned to Items, so it does not know it's of rootHidden type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack. And in a follow up PR, the root hidden issue can be fixed.

Sorry, this comment did not display for me when I was investigating. I don't agree. This will break mechanism for all Collections that are in rootHidden objects. The else block can be removed when we have the fix already, so that we won't be changing the behavior from working to not working.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

else
{
// a collection - expose elements to be of array scheme
varmemberNamedItems=members
.Where(m =>m["name"]?.Value<string>()=="Items"||m["name"]?.Value<string>()=="_items")
.FirstOrDefault();
if(memberNamedItemsis not null&&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(),outDotnetObjectIditemsObjectId))&&
itemsObjectId.Scheme=="array")
{
rootObjectId=itemsObjectId;
}
}

This code is very explicitly looking for a Items member which is an array. So, the else arm is affecting only the very specific types that have such a member, which is the case for List<T>. The proxy, or the debugger agent aren't populating it as an extra thing.

I agree that we shouldn't break the behavior in this PR. But it is incorrect. We want to avoid depending on magic like that, especially when proper mechanisms are available to get the information - DebuggerTypeProxy in this case.

The rootHidden issue is in addition to this, and that's fine to fix separately.

.FirstOrDefault();
if (memberNamedItems is not null &&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId)) &&
DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId) &&
itemsObjectId.Scheme == "array")
{
rootObjectId = itemsObjectId;
Expand DownExpand Up@@ -550,7 +550,8 @@ public static async Task<GetMembersResult> 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;
}
Expand Down
162 changes: 100 additions & 62 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2105,8 +2105,7 @@ public async Task<int> GetTypeByName(string typeToSearch, CancellationToken toke
return retDebuggerCmdReader.ReadInt32();
}

// FIXME: support valuetypes
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int objectId, int typeId, CancellationToken token)
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttributeForObject(int objectId, int typeId, CancellationToken token)
{
try
{
Expand All@@ -2116,25 +2115,11 @@ public async Task<GetMembersResult> 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<string>(), out DotnetObjectId dotnetObjectId))
Expand All@@ -2154,6 +2139,39 @@ public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int obje
return null;
}

public async Task<GetMembersResult> 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<string>(), 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<int> FindDebuggerProxyConstructorIdFor(int typeId, CancellationToken token)
{
try
Expand All@@ -2162,59 +2180,79 @@ private async Task<int> 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<int> 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<int> 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)
Expand Down
4 changes: 1 addition & 3 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,9 +214,7 @@ public async Task<GetMembersResult> 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))
Expand Down
8 changes: 6 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand All@@ -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<string, string>", description: "Count = 3");
props = await GetObjectOnFrame(frame, "openWith");
Assert.Equal(1, props.Count());
Expand DownExpand Up@@ -106,7 +110,7 @@ async Task<bool> CheckProperties(JObject pause_location)
Assert.True(task.Result);
}
}

[ConditionalFact(nameof(RunningOnChrome))]
public async Task InspectObjectOfTypeWithToStringOverriden()
{
Expand Down
12 changes: 10 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>(), $"{label}-[{i}].name");
string exp_name = exp_i["name"]?.Value<string>();
if (string.IsNullOrEmpty(exp_name))
exp_name = i.ToString();

AssertEqual(exp_name, act_i["name"]?.Value<string>(), $"{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;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [wasm][debugger] Fix DebuggerTypeProxy for structs and for classes with multiple constructors by ilonatommy · Pull Request #77039 · dotnet/runtime · GitHub
Skip to content
Original file line numberDiff line numberDiff line change
Expand Up@@ -150,10 +150,10 @@ private static async Task<JArray> GetRootHiddenChildren(
{
// a collection - expose elements to be of array scheme
var memberNamedItems = members
.Where(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items")
.Where(m => m["name"]?.Value<string>() == "Items")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what cases(example code) would we be executing this block?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

All cases in DebuggerTests.EvaluateOnCallFrameTests.EvaluateBrowsableRootHidden because

Error Message:
[] Could not find variable 'listRootHidden[0]'

This PR fixes only the problem with Items vs _items (now it's always Items). But it does not fix the fact that they need to get expanded manually - rootHidden info does not exist on them and cannot be easily extracted. The issue where we address it is this one: #76876.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is my understanding here correct?:

  1. List<T> has [DebuggerTypeProxy(typeof(ICollectionDebugView<>))], so we look at the proxy
  2. the proxy has Items property
  3. Items has [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)], which isn't working yet ([wasm][debugger] RootHidden is not working correctly in DebuggerTypeProxy #76876)
  4. So, we are expanding Items here to work specifically for List<T>?

If so, then is this only to get EvaluateBrowsableRootHidden passing?
Also, does #76876 still happen after the DebuggerTypeProxy fix here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack.
And in a follow up PR, the root hidden issue can be fixed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would say it's a misunderstanding caused by me not deleting the dependency on non-fixed values in the tests properly. Manual expansion was not done on an object decorated with RootHidden but on testPropertiesNone. The else block is not to make the tests pass, it is necessary till RootHidden on Items will be received correctly by Proxy. It was the test that was not 100% logical.
I think now it will make more sense after the last commit.

Yes, the issue is still here. Proxy is not receiving any DebuggerAttributes assigned to Items, so it does not know it's of rootHidden type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack. And in a follow up PR, the root hidden issue can be fixed.

Sorry, this comment did not display for me when I was investigating. I don't agree. This will break mechanism for all Collections that are in rootHidden objects. The else block can be removed when we have the fix already, so that we won't be changing the behavior from working to not working.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

else
{
// a collection - expose elements to be of array scheme
varmemberNamedItems=members
.Where(m =>m["name"]?.Value<string>()=="Items"||m["name"]?.Value<string>()=="_items")
.FirstOrDefault();
if(memberNamedItemsis not null&&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(),outDotnetObjectIditemsObjectId))&&
itemsObjectId.Scheme=="array")
{
rootObjectId=itemsObjectId;
}
}

This code is very explicitly looking for a Items member which is an array. So, the else arm is affecting only the very specific types that have such a member, which is the case for List<T>. The proxy, or the debugger agent aren't populating it as an extra thing.

I agree that we shouldn't break the behavior in this PR. But it is incorrect. We want to avoid depending on magic like that, especially when proper mechanisms are available to get the information - DebuggerTypeProxy in this case.

The rootHidden issue is in addition to this, and that's fine to fix separately.

.FirstOrDefault();
if (memberNamedItems is not null &&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId)) &&
DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId) &&
itemsObjectId.Scheme == "array")
{
rootObjectId = itemsObjectId;
Expand DownExpand Up@@ -550,7 +550,8 @@ public static async Task<GetMembersResult> 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;
}
Expand Down
162 changes: 100 additions & 62 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2105,8 +2105,7 @@ public async Task<int> GetTypeByName(string typeToSearch, CancellationToken toke
return retDebuggerCmdReader.ReadInt32();
}

// FIXME: support valuetypes
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int objectId, int typeId, CancellationToken token)
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttributeForObject(int objectId, int typeId, CancellationToken token)
{
try
{
Expand All@@ -2116,25 +2115,11 @@ public async Task<GetMembersResult> 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<string>(), out DotnetObjectId dotnetObjectId))
Expand All@@ -2154,6 +2139,39 @@ public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int obje
return null;
}

public async Task<GetMembersResult> 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<string>(), 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<int> FindDebuggerProxyConstructorIdFor(int typeId, CancellationToken token)
{
try
Expand All@@ -2162,59 +2180,79 @@ private async Task<int> 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<int> 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<int> 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)
Expand Down
4 changes: 1 addition & 3 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,9 +214,7 @@ public async Task<GetMembersResult> 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))
Expand Down
8 changes: 6 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand All@@ -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<string, string>", description: "Count = 3");
props = await GetObjectOnFrame(frame, "openWith");
Assert.Equal(1, props.Count());
Expand DownExpand Up@@ -106,7 +110,7 @@ async Task<bool> CheckProperties(JObject pause_location)
Assert.True(task.Result);
}
}

[ConditionalFact(nameof(RunningOnChrome))]
public async Task InspectObjectOfTypeWithToStringOverriden()
{
Expand Down
12 changes: 10 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>(), $"{label}-[{i}].name");
string exp_name = exp_i["name"]?.Value<string>();
if (string.IsNullOrEmpty(exp_name))
exp_name = i.ToString();

AssertEqual(exp_name, act_i["name"]?.Value<string>(), $"{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;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [wasm][debugger] Fix DebuggerTypeProxy for structs and for classes with multiple constructors by ilonatommy · Pull Request #77039 · dotnet/runtime · GitHub
Skip to content
Original file line numberDiff line numberDiff line change
Expand Up@@ -150,10 +150,10 @@ private static async Task<JArray> GetRootHiddenChildren(
{
// a collection - expose elements to be of array scheme
var memberNamedItems = members
.Where(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items")
.Where(m => m["name"]?.Value<string>() == "Items")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what cases(example code) would we be executing this block?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

All cases in DebuggerTests.EvaluateOnCallFrameTests.EvaluateBrowsableRootHidden because

Error Message:
[] Could not find variable 'listRootHidden[0]'

This PR fixes only the problem with Items vs _items (now it's always Items). But it does not fix the fact that they need to get expanded manually - rootHidden info does not exist on them and cannot be easily extracted. The issue where we address it is this one: #76876.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is my understanding here correct?:

  1. List<T> has [DebuggerTypeProxy(typeof(ICollectionDebugView<>))], so we look at the proxy
  2. the proxy has Items property
  3. Items has [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)], which isn't working yet ([wasm][debugger] RootHidden is not working correctly in DebuggerTypeProxy #76876)
  4. So, we are expanding Items here to work specifically for List<T>?

If so, then is this only to get EvaluateBrowsableRootHidden passing?
Also, does #76876 still happen after the DebuggerTypeProxy fix here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack.
And in a follow up PR, the root hidden issue can be fixed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would say it's a misunderstanding caused by me not deleting the dependency on non-fixed values in the tests properly. Manual expansion was not done on an object decorated with RootHidden but on testPropertiesNone. The else block is not to make the tests pass, it is necessary till RootHidden on Items will be received correctly by Proxy. It was the test that was not 100% logical.
I think now it will make more sense after the last commit.

Yes, the issue is still here. Proxy is not receiving any DebuggerAttributes assigned to Items, so it does not know it's of rootHidden type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack. And in a follow up PR, the root hidden issue can be fixed.

Sorry, this comment did not display for me when I was investigating. I don't agree. This will break mechanism for all Collections that are in rootHidden objects. The else block can be removed when we have the fix already, so that we won't be changing the behavior from working to not working.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

else
{
// a collection - expose elements to be of array scheme
varmemberNamedItems=members
.Where(m =>m["name"]?.Value<string>()=="Items"||m["name"]?.Value<string>()=="_items")
.FirstOrDefault();
if(memberNamedItemsis not null&&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(),outDotnetObjectIditemsObjectId))&&
itemsObjectId.Scheme=="array")
{
rootObjectId=itemsObjectId;
}
}

This code is very explicitly looking for a Items member which is an array. So, the else arm is affecting only the very specific types that have such a member, which is the case for List<T>. The proxy, or the debugger agent aren't populating it as an extra thing.

I agree that we shouldn't break the behavior in this PR. But it is incorrect. We want to avoid depending on magic like that, especially when proper mechanisms are available to get the information - DebuggerTypeProxy in this case.

The rootHidden issue is in addition to this, and that's fine to fix separately.

.FirstOrDefault();
if (memberNamedItems is not null &&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId)) &&
DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId) &&
itemsObjectId.Scheme == "array")
{
rootObjectId = itemsObjectId;
Expand DownExpand Up@@ -550,7 +550,8 @@ public static async Task<GetMembersResult> 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;
}
Expand Down
162 changes: 100 additions & 62 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2105,8 +2105,7 @@ public async Task<int> GetTypeByName(string typeToSearch, CancellationToken toke
return retDebuggerCmdReader.ReadInt32();
}

// FIXME: support valuetypes
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int objectId, int typeId, CancellationToken token)
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttributeForObject(int objectId, int typeId, CancellationToken token)
{
try
{
Expand All@@ -2116,25 +2115,11 @@ public async Task<GetMembersResult> 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<string>(), out DotnetObjectId dotnetObjectId))
Expand All@@ -2154,6 +2139,39 @@ public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int obje
return null;
}

public async Task<GetMembersResult> 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<string>(), 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<int> FindDebuggerProxyConstructorIdFor(int typeId, CancellationToken token)
{
try
Expand All@@ -2162,59 +2180,79 @@ private async Task<int> 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<int> 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<int> 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)
Expand Down
4 changes: 1 addition & 3 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,9 +214,7 @@ public async Task<GetMembersResult> 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))
Expand Down
8 changes: 6 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand All@@ -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<string, string>", description: "Count = 3");
props = await GetObjectOnFrame(frame, "openWith");
Assert.Equal(1, props.Count());
Expand DownExpand Up@@ -106,7 +110,7 @@ async Task<bool> CheckProperties(JObject pause_location)
Assert.True(task.Result);
}
}

[ConditionalFact(nameof(RunningOnChrome))]
public async Task InspectObjectOfTypeWithToStringOverriden()
{
Expand Down
12 changes: 10 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>(), $"{label}-[{i}].name");
string exp_name = exp_i["name"]?.Value<string>();
if (string.IsNullOrEmpty(exp_name))
exp_name = i.ToString();

AssertEqual(exp_name, act_i["name"]?.Value<string>(), $"{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;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [wasm][debugger] Fix DebuggerTypeProxy for structs and for classes with multiple constructors by ilonatommy · Pull Request #77039 · dotnet/runtime · GitHub
Skip to content
Original file line numberDiff line numberDiff line change
Expand Up@@ -150,10 +150,10 @@ private static async Task<JArray> GetRootHiddenChildren(
{
// a collection - expose elements to be of array scheme
var memberNamedItems = members
.Where(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items")
.Where(m => m["name"]?.Value<string>() == "Items")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what cases(example code) would we be executing this block?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

All cases in DebuggerTests.EvaluateOnCallFrameTests.EvaluateBrowsableRootHidden because

Error Message:
[] Could not find variable 'listRootHidden[0]'

This PR fixes only the problem with Items vs _items (now it's always Items). But it does not fix the fact that they need to get expanded manually - rootHidden info does not exist on them and cannot be easily extracted. The issue where we address it is this one: #76876.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is my understanding here correct?:

  1. List<T> has [DebuggerTypeProxy(typeof(ICollectionDebugView<>))], so we look at the proxy
  2. the proxy has Items property
  3. Items has [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)], which isn't working yet ([wasm][debugger] RootHidden is not working correctly in DebuggerTypeProxy #76876)
  4. So, we are expanding Items here to work specifically for List<T>?

If so, then is this only to get EvaluateBrowsableRootHidden passing?
Also, does #76876 still happen after the DebuggerTypeProxy fix here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack.
And in a follow up PR, the root hidden issue can be fixed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would say it's a misunderstanding caused by me not deleting the dependency on non-fixed values in the tests properly. Manual expansion was not done on an object decorated with RootHidden but on testPropertiesNone. The else block is not to make the tests pass, it is necessary till RootHidden on Items will be received correctly by Proxy. It was the test that was not 100% logical.
I think now it will make more sense after the last commit.

Yes, the issue is still here. Proxy is not receiving any DebuggerAttributes assigned to Items, so it does not know it's of rootHidden type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack. And in a follow up PR, the root hidden issue can be fixed.

Sorry, this comment did not display for me when I was investigating. I don't agree. This will break mechanism for all Collections that are in rootHidden objects. The else block can be removed when we have the fix already, so that we won't be changing the behavior from working to not working.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

else
{
// a collection - expose elements to be of array scheme
varmemberNamedItems=members
.Where(m =>m["name"]?.Value<string>()=="Items"||m["name"]?.Value<string>()=="_items")
.FirstOrDefault();
if(memberNamedItemsis not null&&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(),outDotnetObjectIditemsObjectId))&&
itemsObjectId.Scheme=="array")
{
rootObjectId=itemsObjectId;
}
}

This code is very explicitly looking for a Items member which is an array. So, the else arm is affecting only the very specific types that have such a member, which is the case for List<T>. The proxy, or the debugger agent aren't populating it as an extra thing.

I agree that we shouldn't break the behavior in this PR. But it is incorrect. We want to avoid depending on magic like that, especially when proper mechanisms are available to get the information - DebuggerTypeProxy in this case.

The rootHidden issue is in addition to this, and that's fine to fix separately.

.FirstOrDefault();
if (memberNamedItems is not null &&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId)) &&
DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId) &&
itemsObjectId.Scheme == "array")
{
rootObjectId = itemsObjectId;
Expand DownExpand Up@@ -550,7 +550,8 @@ public static async Task<GetMembersResult> 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;
}
Expand Down
162 changes: 100 additions & 62 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2105,8 +2105,7 @@ public async Task<int> GetTypeByName(string typeToSearch, CancellationToken toke
return retDebuggerCmdReader.ReadInt32();
}

// FIXME: support valuetypes
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int objectId, int typeId, CancellationToken token)
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttributeForObject(int objectId, int typeId, CancellationToken token)
{
try
{
Expand All@@ -2116,25 +2115,11 @@ public async Task<GetMembersResult> 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<string>(), out DotnetObjectId dotnetObjectId))
Expand All@@ -2154,6 +2139,39 @@ public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int obje
return null;
}

public async Task<GetMembersResult> 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<string>(), 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<int> FindDebuggerProxyConstructorIdFor(int typeId, CancellationToken token)
{
try
Expand All@@ -2162,59 +2180,79 @@ private async Task<int> 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<int> 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<int> 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)
Expand Down
4 changes: 1 addition & 3 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,9 +214,7 @@ public async Task<GetMembersResult> 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))
Expand Down
8 changes: 6 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand All@@ -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<string, string>", description: "Count = 3");
props = await GetObjectOnFrame(frame, "openWith");
Assert.Equal(1, props.Count());
Expand DownExpand Up@@ -106,7 +110,7 @@ async Task<bool> CheckProperties(JObject pause_location)
Assert.True(task.Result);
}
}

[ConditionalFact(nameof(RunningOnChrome))]
public async Task InspectObjectOfTypeWithToStringOverriden()
{
Expand Down
12 changes: 10 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>(), $"{label}-[{i}].name");
string exp_name = exp_i["name"]?.Value<string>();
if (string.IsNullOrEmpty(exp_name))
exp_name = i.ToString();

AssertEqual(exp_name, act_i["name"]?.Value<string>(), $"{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;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [wasm][debugger] Fix DebuggerTypeProxy for structs and for classes with multiple constructors by ilonatommy · Pull Request #77039 · dotnet/runtime · GitHub
Skip to content
Original file line numberDiff line numberDiff line change
Expand Up@@ -150,10 +150,10 @@ private static async Task<JArray> GetRootHiddenChildren(
{
// a collection - expose elements to be of array scheme
var memberNamedItems = members
.Where(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items")
.Where(m => m["name"]?.Value<string>() == "Items")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what cases(example code) would we be executing this block?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

All cases in DebuggerTests.EvaluateOnCallFrameTests.EvaluateBrowsableRootHidden because

Error Message:
[] Could not find variable 'listRootHidden[0]'

This PR fixes only the problem with Items vs _items (now it's always Items). But it does not fix the fact that they need to get expanded manually - rootHidden info does not exist on them and cannot be easily extracted. The issue where we address it is this one: #76876.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is my understanding here correct?:

  1. List<T> has [DebuggerTypeProxy(typeof(ICollectionDebugView<>))], so we look at the proxy
  2. the proxy has Items property
  3. Items has [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)], which isn't working yet ([wasm][debugger] RootHidden is not working correctly in DebuggerTypeProxy #76876)
  4. So, we are expanding Items here to work specifically for List<T>?

If so, then is this only to get EvaluateBrowsableRootHidden passing?
Also, does #76876 still happen after the DebuggerTypeProxy fix here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack.
And in a follow up PR, the root hidden issue can be fixed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would say it's a misunderstanding caused by me not deleting the dependency on non-fixed values in the tests properly. Manual expansion was not done on an object decorated with RootHidden but on testPropertiesNone. The else block is not to make the tests pass, it is necessary till RootHidden on Items will be received correctly by Proxy. It was the test that was not 100% logical.
I think now it will make more sense after the last commit.

Yes, the issue is still here. Proxy is not receiving any DebuggerAttributes assigned to Items, so it does not know it's of rootHidden type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack. And in a follow up PR, the root hidden issue can be fixed.

Sorry, this comment did not display for me when I was investigating. I don't agree. This will break mechanism for all Collections that are in rootHidden objects. The else block can be removed when we have the fix already, so that we won't be changing the behavior from working to not working.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

else
{
// a collection - expose elements to be of array scheme
varmemberNamedItems=members
.Where(m =>m["name"]?.Value<string>()=="Items"||m["name"]?.Value<string>()=="_items")
.FirstOrDefault();
if(memberNamedItemsis not null&&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(),outDotnetObjectIditemsObjectId))&&
itemsObjectId.Scheme=="array")
{
rootObjectId=itemsObjectId;
}
}

This code is very explicitly looking for a Items member which is an array. So, the else arm is affecting only the very specific types that have such a member, which is the case for List<T>. The proxy, or the debugger agent aren't populating it as an extra thing.

I agree that we shouldn't break the behavior in this PR. But it is incorrect. We want to avoid depending on magic like that, especially when proper mechanisms are available to get the information - DebuggerTypeProxy in this case.

The rootHidden issue is in addition to this, and that's fine to fix separately.

.FirstOrDefault();
if (memberNamedItems is not null &&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId)) &&
DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId) &&
itemsObjectId.Scheme == "array")
{
rootObjectId = itemsObjectId;
Expand DownExpand Up@@ -550,7 +550,8 @@ public static async Task<GetMembersResult> 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;
}
Expand Down
162 changes: 100 additions & 62 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2105,8 +2105,7 @@ public async Task<int> GetTypeByName(string typeToSearch, CancellationToken toke
return retDebuggerCmdReader.ReadInt32();
}

// FIXME: support valuetypes
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int objectId, int typeId, CancellationToken token)
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttributeForObject(int objectId, int typeId, CancellationToken token)
{
try
{
Expand All@@ -2116,25 +2115,11 @@ public async Task<GetMembersResult> 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<string>(), out DotnetObjectId dotnetObjectId))
Expand All@@ -2154,6 +2139,39 @@ public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int obje
return null;
}

public async Task<GetMembersResult> 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<string>(), 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<int> FindDebuggerProxyConstructorIdFor(int typeId, CancellationToken token)
{
try
Expand All@@ -2162,59 +2180,79 @@ private async Task<int> 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<int> 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<int> 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)
Expand Down
4 changes: 1 addition & 3 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,9 +214,7 @@ public async Task<GetMembersResult> 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))
Expand Down
8 changes: 6 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand All@@ -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<string, string>", description: "Count = 3");
props = await GetObjectOnFrame(frame, "openWith");
Assert.Equal(1, props.Count());
Expand DownExpand Up@@ -106,7 +110,7 @@ async Task<bool> CheckProperties(JObject pause_location)
Assert.True(task.Result);
}
}

[ConditionalFact(nameof(RunningOnChrome))]
public async Task InspectObjectOfTypeWithToStringOverriden()
{
Expand Down
12 changes: 10 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>(), $"{label}-[{i}].name");
string exp_name = exp_i["name"]?.Value<string>();
if (string.IsNullOrEmpty(exp_name))
exp_name = i.ToString();

AssertEqual(exp_name, act_i["name"]?.Value<string>(), $"{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;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [wasm][debugger] Fix DebuggerTypeProxy for structs and for classes with multiple constructors by ilonatommy · Pull Request #77039 · dotnet/runtime · GitHub
Skip to content
Original file line numberDiff line numberDiff line change
Expand Up@@ -150,10 +150,10 @@ private static async Task<JArray> GetRootHiddenChildren(
{
// a collection - expose elements to be of array scheme
var memberNamedItems = members
.Where(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items")
.Where(m => m["name"]?.Value<string>() == "Items")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what cases(example code) would we be executing this block?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

All cases in DebuggerTests.EvaluateOnCallFrameTests.EvaluateBrowsableRootHidden because

Error Message:
[] Could not find variable 'listRootHidden[0]'

This PR fixes only the problem with Items vs _items (now it's always Items). But it does not fix the fact that they need to get expanded manually - rootHidden info does not exist on them and cannot be easily extracted. The issue where we address it is this one: #76876.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is my understanding here correct?:

  1. List<T> has [DebuggerTypeProxy(typeof(ICollectionDebugView<>))], so we look at the proxy
  2. the proxy has Items property
  3. Items has [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)], which isn't working yet ([wasm][debugger] RootHidden is not working correctly in DebuggerTypeProxy #76876)
  4. So, we are expanding Items here to work specifically for List<T>?

If so, then is this only to get EvaluateBrowsableRootHidden passing?
Also, does #76876 still happen after the DebuggerTypeProxy fix here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack.
And in a follow up PR, the root hidden issue can be fixed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would say it's a misunderstanding caused by me not deleting the dependency on non-fixed values in the tests properly. Manual expansion was not done on an object decorated with RootHidden but on testPropertiesNone. The else block is not to make the tests pass, it is necessary till RootHidden on Items will be received correctly by Proxy. It was the test that was not 100% logical.
I think now it will make more sense after the last commit.

Yes, the issue is still here. Proxy is not receiving any DebuggerAttributes assigned to Items, so it does not know it's of rootHidden type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack. And in a follow up PR, the root hidden issue can be fixed.

Sorry, this comment did not display for me when I was investigating. I don't agree. This will break mechanism for all Collections that are in rootHidden objects. The else block can be removed when we have the fix already, so that we won't be changing the behavior from working to not working.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

else
{
// a collection - expose elements to be of array scheme
varmemberNamedItems=members
.Where(m =>m["name"]?.Value<string>()=="Items"||m["name"]?.Value<string>()=="_items")
.FirstOrDefault();
if(memberNamedItemsis not null&&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(),outDotnetObjectIditemsObjectId))&&
itemsObjectId.Scheme=="array")
{
rootObjectId=itemsObjectId;
}
}

This code is very explicitly looking for a Items member which is an array. So, the else arm is affecting only the very specific types that have such a member, which is the case for List<T>. The proxy, or the debugger agent aren't populating it as an extra thing.

I agree that we shouldn't break the behavior in this PR. But it is incorrect. We want to avoid depending on magic like that, especially when proper mechanisms are available to get the information - DebuggerTypeProxy in this case.

The rootHidden issue is in addition to this, and that's fine to fix separately.

.FirstOrDefault();
if (memberNamedItems is not null &&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId)) &&
DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId) &&
itemsObjectId.Scheme == "array")
{
rootObjectId = itemsObjectId;
Expand DownExpand Up@@ -550,7 +550,8 @@ public static async Task<GetMembersResult> 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;
}
Expand Down
162 changes: 100 additions & 62 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2105,8 +2105,7 @@ public async Task<int> GetTypeByName(string typeToSearch, CancellationToken toke
return retDebuggerCmdReader.ReadInt32();
}

// FIXME: support valuetypes
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int objectId, int typeId, CancellationToken token)
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttributeForObject(int objectId, int typeId, CancellationToken token)
{
try
{
Expand All@@ -2116,25 +2115,11 @@ public async Task<GetMembersResult> 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<string>(), out DotnetObjectId dotnetObjectId))
Expand All@@ -2154,6 +2139,39 @@ public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int obje
return null;
}

public async Task<GetMembersResult> 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<string>(), 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<int> FindDebuggerProxyConstructorIdFor(int typeId, CancellationToken token)
{
try
Expand All@@ -2162,59 +2180,79 @@ private async Task<int> 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<int> 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<int> 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)
Expand Down
4 changes: 1 addition & 3 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,9 +214,7 @@ public async Task<GetMembersResult> 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))
Expand Down
8 changes: 6 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand All@@ -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<string, string>", description: "Count = 3");
props = await GetObjectOnFrame(frame, "openWith");
Assert.Equal(1, props.Count());
Expand DownExpand Up@@ -106,7 +110,7 @@ async Task<bool> CheckProperties(JObject pause_location)
Assert.True(task.Result);
}
}

[ConditionalFact(nameof(RunningOnChrome))]
public async Task InspectObjectOfTypeWithToStringOverriden()
{
Expand Down
12 changes: 10 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>(), $"{label}-[{i}].name");
string exp_name = exp_i["name"]?.Value<string>();
if (string.IsNullOrEmpty(exp_name))
exp_name = i.ToString();

AssertEqual(exp_name, act_i["name"]?.Value<string>(), $"{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;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [wasm][debugger] Fix DebuggerTypeProxy for structs and for classes with multiple constructors by ilonatommy · Pull Request #77039 · dotnet/runtime · GitHub
Skip to content
Original file line numberDiff line numberDiff line change
Expand Up@@ -150,10 +150,10 @@ private static async Task<JArray> GetRootHiddenChildren(
{
// a collection - expose elements to be of array scheme
var memberNamedItems = members
.Where(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items")
.Where(m => m["name"]?.Value<string>() == "Items")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what cases(example code) would we be executing this block?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

All cases in DebuggerTests.EvaluateOnCallFrameTests.EvaluateBrowsableRootHidden because

Error Message:
[] Could not find variable 'listRootHidden[0]'

This PR fixes only the problem with Items vs _items (now it's always Items). But it does not fix the fact that they need to get expanded manually - rootHidden info does not exist on them and cannot be easily extracted. The issue where we address it is this one: #76876.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is my understanding here correct?:

  1. List<T> has [DebuggerTypeProxy(typeof(ICollectionDebugView<>))], so we look at the proxy
  2. the proxy has Items property
  3. Items has [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)], which isn't working yet ([wasm][debugger] RootHidden is not working correctly in DebuggerTypeProxy #76876)
  4. So, we are expanding Items here to work specifically for List<T>?

If so, then is this only to get EvaluateBrowsableRootHidden passing?
Also, does #76876 still happen after the DebuggerTypeProxy fix here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack.
And in a follow up PR, the root hidden issue can be fixed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I would say it's a misunderstanding caused by me not deleting the dependency on non-fixed values in the tests properly. Manual expansion was not done on an object decorated with RootHidden but on testPropertiesNone. The else block is not to make the tests pass, it is necessary till RootHidden on Items will be received correctly by Proxy. It was the test that was not 100% logical.
I think now it will make more sense after the last commit.

Yes, the issue is still here. Proxy is not receiving any DebuggerAttributes assigned to Items, so it does not know it's of rootHidden type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I looked at it, and (4) is true. The test for listRootHidden should be disabled for now, and a comment mentioning the issue. And we can remove this incorrect hack. And in a follow up PR, the root hidden issue can be fixed.

Sorry, this comment did not display for me when I was investigating. I don't agree. This will break mechanism for all Collections that are in rootHidden objects. The else block can be removed when we have the fix already, so that we won't be changing the behavior from working to not working.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

else
{
// a collection - expose elements to be of array scheme
varmemberNamedItems=members
.Where(m =>m["name"]?.Value<string>()=="Items"||m["name"]?.Value<string>()=="_items")
.FirstOrDefault();
if(memberNamedItemsis not null&&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(),outDotnetObjectIditemsObjectId))&&
itemsObjectId.Scheme=="array")
{
rootObjectId=itemsObjectId;
}
}

This code is very explicitly looking for a Items member which is an array. So, the else arm is affecting only the very specific types that have such a member, which is the case for List<T>. The proxy, or the debugger agent aren't populating it as an extra thing.

I agree that we shouldn't break the behavior in this PR. But it is incorrect. We want to avoid depending on magic like that, especially when proper mechanisms are available to get the information - DebuggerTypeProxy in this case.

The rootHidden issue is in addition to this, and that's fine to fix separately.

.FirstOrDefault();
if (memberNamedItems is not null &&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId)) &&
DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId) &&
itemsObjectId.Scheme == "array")
{
rootObjectId = itemsObjectId;
Expand DownExpand Up@@ -550,7 +550,8 @@ public static async Task<GetMembersResult> 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;
}
Expand Down
162 changes: 100 additions & 62 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2105,8 +2105,7 @@ public async Task<int> GetTypeByName(string typeToSearch, CancellationToken toke
return retDebuggerCmdReader.ReadInt32();
}

// FIXME: support valuetypes
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int objectId, int typeId, CancellationToken token)
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttributeForObject(int objectId, int typeId, CancellationToken token)
{
try
{
Expand All@@ -2116,25 +2115,11 @@ public async Task<GetMembersResult> 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<string>(), out DotnetObjectId dotnetObjectId))
Expand All@@ -2154,6 +2139,39 @@ public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int obje
return null;
}

public async Task<GetMembersResult> 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<string>(), 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<int> FindDebuggerProxyConstructorIdFor(int typeId, CancellationToken token)
{
try
Expand All@@ -2162,59 +2180,79 @@ private async Task<int> 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<int> 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<int> 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)
Expand Down
4 changes: 1 addition & 3 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,9 +214,7 @@ public async Task<GetMembersResult> 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))
Expand Down
8 changes: 6 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand All@@ -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<string, string>", description: "Count = 3");
props = await GetObjectOnFrame(frame, "openWith");
Assert.Equal(1, props.Count());
Expand DownExpand Up@@ -106,7 +110,7 @@ async Task<bool> CheckProperties(JObject pause_location)
Assert.True(task.Result);
}
}

[ConditionalFact(nameof(RunningOnChrome))]
public async Task InspectObjectOfTypeWithToStringOverriden()
{
Expand Down
12 changes: 10 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>(), $"{label}-[{i}].name");
string exp_name = exp_i["name"]?.Value<string>();
if (string.IsNullOrEmpty(exp_name))
exp_name = i.ToString();

AssertEqual(exp_name, act_i["name"]?.Value<string>(), $"{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;
}
Expand Down
Loading