Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wasm][debugger] Implement support to DebuggerDisplay attribute #55524

Merged
merged 6 commits into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/mono/sample/wasm/browser/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,40 @@

using System;
using System.Runtime.CompilerServices;
using System.Diagnostics;

namespace Sample
{
public class Test
{
[DebuggerDisplay ("Some {Val1} Value {Val2} End")]
class WithDisplayString
{
internal string Val1 = "one";

public int Val2 { get { return 2; } }
}

class WithToString
{
public override string ToString ()
{
return "SomeString";
}
}

[DebuggerDisplay ("{GetDebuggerDisplay(), nq}")]
class DebuggerDisplayMethodTest
{
int someInt = 32;
int someInt2 = 43;

string GetDebuggerDisplay ()
{
return "First Int:" + someInt + " Second Int:" + someInt2;
}
}

public static void Main(string[] args)
{
Console.WriteLine ("Hello, World!");
Expand All @@ -16,6 +45,10 @@ public static void Main(string[] args)
[MethodImpl(MethodImplOptions.NoInlining)]
public static int TestMeaning()
{
var a = new WithDisplayString();
var c = new DebuggerDisplayMethodTest();
Console.WriteLine(a);
Console.WriteLine(c);
return 42;
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,5 +322,16 @@ internal class PerScopeCache
{
public Dictionary<string, JObject> Locals { get; } = new Dictionary<string, JObject>();
public Dictionary<string, JObject> MemberReferences { get; } = new Dictionary<string, JObject>();
public Dictionary<string, JObject> ObjectFields { get; } = new Dictionary<string, JObject>();
public PerScopeCache(JArray objectValues)
{
foreach (var objectValue in objectValues)
{
ObjectFields[objectValue["name"].Value<string>()] = objectValue.Value<JObject>();
}
}
public PerScopeCache()
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public override void Visit(SyntaxNode node)

if (node is IdentifierNameSyntax identifier
&& !(identifier.Parent is MemberAccessExpressionSyntax)
&& !(identifier.Parent is InvocationExpressionSyntax)
&& !identifiers.Any(x => x.Identifier.Text == identifier.Identifier.Text))
{
identifiers.Add(identifier);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal class MemberReferenceResolver
private ExecutionContext ctx;
private PerScopeCache scopeCache;
private ILogger logger;
private bool locals_fetched;
private bool localsFetched;

public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, SessionId sessionId, int scopeId, ILogger logger)
{
Expand All @@ -33,6 +33,18 @@ public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, SessionId
this.logger = logger;
scopeCache = ctx.GetCacheForScope(scopeId);
}

public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, SessionId sessionId, JArray objectValues, ILogger logger)
{
this.sessionId = sessionId;
scopeId = -1;
this.proxy = proxy;
this.ctx = ctx;
this.logger = logger;
scopeCache = new PerScopeCache(objectValues);
localsFetched = true;
}

public async Task<JObject> GetValueFromObject(JToken objRet, CancellationToken token)
{
if (objRet["value"]?["className"]?.Value<string>() == "System.Exception")
Expand Down Expand Up @@ -76,6 +88,11 @@ public async Task<JObject> Resolve(string varName, CancellationToken token)
if (scopeCache.MemberReferences.TryGetValue(varName, out JObject ret)) {
return ret;
}

if (scopeCache.ObjectFields.TryGetValue(varName, out JObject valueRet)) {
return await GetValueFromObject(valueRet, token);
}

foreach (string part in parts)
{
string partTrimmed = part.Trim();
Expand All @@ -96,12 +113,12 @@ public async Task<JObject> Resolve(string varName, CancellationToken token)
}
continue;
}
if (scopeCache.Locals.Count == 0 && !locals_fetched)
if (scopeCache.Locals.Count == 0 && !localsFetched)
{
Result scope_res = await proxy.GetScopeProperties(sessionId, scopeId, token);
if (scope_res.IsErr)
throw new Exception($"BUG: Unable to get properties for scope: {scopeId}. {scope_res}");
locals_fetched = true;
localsFetched = true;
}
if (scopeCache.Locals.TryGetValue(partTrimmed, out JObject obj))
{
Expand Down Expand Up @@ -145,6 +162,12 @@ public async Task<JObject> Resolve(InvocationExpressionSyntax method, Dictionary
rootObject = await Resolve(memberAccessExpressionSyntax.Expression.ToString(), token);
methodName = memberAccessExpressionSyntax.Name.ToString();
}
else if (expr is IdentifierNameSyntax)
if (scopeCache.ObjectFields.TryGetValue("this", out JObject valueRet)) {
rootObject = await GetValueFromObject(valueRet, token);
methodName = expr.ToString();
}

if (rootObject != null)
{
DotnetObjectId.TryParse(rootObject?["objectId"]?.Value<string>(), out DotnetObjectId objectId);
Expand Down
2 changes: 1 addition & 1 deletion src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ internal class MonoProxy : DevToolsProxy
public MonoProxy(ILoggerFactory loggerFactory, IList<string> urlSymbolServerList) : base(loggerFactory)
{
this.urlSymbolServerList = urlSymbolServerList ?? new List<string>();
SdbHelper = new MonoSDBHelper(this);
SdbHelper = new MonoSDBHelper(this, logger);
}

internal ExecutionContext GetContext(SessionId sessionId)
Expand Down
95 changes: 93 additions & 2 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -615,9 +615,12 @@ internal class MonoSDBHelper
private MonoProxy proxy;
private static int MINOR_VERSION = 61;
private static int MAJOR_VERSION = 2;
public MonoSDBHelper(MonoProxy proxy)
private readonly ILogger logger;

public MonoSDBHelper(MonoProxy proxy, ILogger logger)
{
this.proxy = proxy;
this.logger = logger;
}

public void ClearCache()
Expand Down Expand Up @@ -946,6 +949,7 @@ public async Task<List<FieldTypeClass>> GetTypeFields(SessionId sessionId, int t
}
return ret;
}

public string ReplaceCommonClassNames(string className)
{
className = className.Replace("System.String", "string");
Expand All @@ -957,6 +961,88 @@ public string ReplaceCommonClassNames(string className)
className = className.Replace("System.Byte", "byte");
return className;
}

public async Task<string> GetDebuggerDisplayAttribute(SessionId sessionId, int objectId, int typeId, CancellationToken token)
{
string expr = "";
var invokeParams = new MemoryStream();
var invokeParamsWriter = new MonoBinaryWriter(invokeParams);
var commandParams = new MemoryStream();
var commandParamsWriter = new MonoBinaryWriter(commandParams);
commandParamsWriter.Write(typeId);
commandParamsWriter.Write(0);
var retDebuggerCmdReader = await SendDebuggerAgentCommand<CmdType>(sessionId, CmdType.GetCattrs, commandParams, token);
var count = retDebuggerCmdReader.ReadInt32();
if (count == 0)
return null;
for (int i = 0 ; i < count; i++)
{
var methodId = retDebuggerCmdReader.ReadInt32();
if (methodId == 0)
continue;
commandParams = new MemoryStream();
commandParamsWriter = new MonoBinaryWriter(commandParams);
commandParamsWriter.Write(methodId);
var retDebuggerCmdReader2 = await SendDebuggerAgentCommand<CmdMethod>(sessionId, CmdMethod.GetDeclaringType, commandParams, token);
var customAttributeTypeId = retDebuggerCmdReader2.ReadInt32();
var customAttributeName = await GetTypeName(sessionId, customAttributeTypeId, token);
if (customAttributeName == "System.Diagnostics.DebuggerDisplayAttribute")
{
invokeParamsWriter.Write((byte)ValueTypeId.Null);
invokeParamsWriter.Write((byte)0); //not used
invokeParamsWriter.Write(0); //not used
var parmCount = retDebuggerCmdReader.ReadInt32();
invokeParamsWriter.Write((int)1);
for (int j = 0; j < parmCount; j++)
{
invokeParamsWriter.Write((byte)retDebuggerCmdReader.ReadByte());
invokeParamsWriter.Write(retDebuggerCmdReader.ReadInt32());
}
var retMethod = await InvokeMethod(sessionId, invokeParams.ToArray(), methodId, "methodRet", token);
DotnetObjectId.TryParse(retMethod?["value"]?["objectId"]?.Value<string>(), out DotnetObjectId dotnetObjectId);
var displayAttrs = await GetObjectValues(sessionId, int.Parse(dotnetObjectId.Value), true, false, false, false, token);
var displayAttrValue = displayAttrs.FirstOrDefault(attr => attr["name"].Value<string>().Equals("Value"));
try {
ExecutionContext context = proxy.GetContext(sessionId);
var objectValues = await GetObjectValues(sessionId, objectId, true, false, false, false, token);

var thisObj = CreateJObject<string>("", "object", "", false, objectId:$"dotnet:object:{objectId}");
thisObj["name"] = "this";
objectValues.Add(thisObj);

var resolver = new MemberReferenceResolver(proxy, context, sessionId, objectValues, logger);
var dispAttrStr = displayAttrValue["value"]?["value"]?.Value<string>();
//bool noQuotes = false;
if (dispAttrStr.Contains(", nq"))
{
//noQuotes = true;
dispAttrStr = dispAttrStr.Replace(", nq", "");
}
expr = "$\"" + dispAttrStr + "\"";
JObject retValue = await resolver.Resolve(expr, token);
if (retValue == null)
retValue = await EvaluateExpression.CompileAndRunTheExpression(expr, resolver, token);
return retValue?["value"]?.Value<string>();
}
catch (Exception)
{
logger.LogDebug($"Could not evaluate DebuggerDisplayAttribute - {expr}");
return null;
}
}
else
{
var parmCount = retDebuggerCmdReader.ReadInt32();
for (int j = 0; j < parmCount; j++)
{
//to read parameters
await CreateJObjectForVariableValue(sessionId, retDebuggerCmdReader, "varName", false, -1, token);
}
}
}
return null;
}

public async Task<string> GetTypeName(SessionId sessionId, int type_id, CancellationToken token)
{
var commandParams = new MemoryStream();
Expand Down Expand Up @@ -1043,7 +1129,7 @@ public async Task<int> GetMethodIdByName(SessionId sessionId, int type_id, strin
var commandParamsWriter = new MonoBinaryWriter(commandParams);
commandParamsWriter.Write((int)type_id);
commandParamsWriter.WriteString(method_name);
commandParamsWriter.Write((int)(0x10 | 4)); //instance methods
commandParamsWriter.Write((int)(0x10 | 4 | 0x20)); //instance methods
commandParamsWriter.Write((int)1); //case sensitive
var retDebuggerCmdReader = await SendDebuggerAgentCommand<CmdType>(sessionId, CmdType.GetMethodsByNameFlags, commandParams, token);
var nMethods = retDebuggerCmdReader.ReadInt32();
Expand Down Expand Up @@ -1299,7 +1385,12 @@ public async Task<JObject> CreateJObjectForObject(SessionId sessionId, MonoBinar
var className = "";
var type_id = await GetTypeIdFromObject(sessionId, objectId, false, token);
className = await GetTypeName(sessionId, type_id[0], token);
var debuggerDisplayAttribute = await GetDebuggerDisplayAttribute(sessionId, objectId, type_id[0], token);
var description = className.ToString();

if (debuggerDisplayAttribute != null)
description = debuggerDisplayAttribute;

if (await IsDelegate(sessionId, objectId, token))
{
if (typeIdFromAttribute != -1)
Expand Down
33 changes: 33 additions & 0 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

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

namespace DebuggerTests
{

public class CustomViewTests : DebuggerTestBase
{
[Fact]
public async Task CustomView()
{
var bp = await SetBreakpointInMethod("debugger-test.dll", "DebuggerTests.DebuggerCustomViewTest", "run", 5);
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",
bp.Value["locations"][0]["lineNumber"].Value<int>(),
bp.Value["locations"][0]["columnNumber"].Value<int>(),
"run");

var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
CheckObject(locals, "a", "DebuggerTests.WithDisplayString", description:"Some one Value 2 End");
CheckObject(locals, "c", "DebuggerTests.DebuggerDisplayMethodTest", description: "First Int:32 Second Int:43");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading.Tasks;
using System.Diagnostics;

namespace DebuggerTests
{
[DebuggerDisplay("Some {Val1} Value {Val2} End")]
class WithDisplayString
{
internal string Val1 = "one";

public int Val2 { get { return 2; } }
}

class WithToString
{
public override string ToString ()
{
return "SomeString";
}
}

[DebuggerTypeProxy(typeof(TheProxy))]
class WithProxy
{
public string Val1 {
get { return "one"; }
}
}

class TheProxy
{
WithProxy wp;

public TheProxy (WithProxy wp)
{
this.wp = wp;
}

public string Val2 {
get { return wp.Val1; }
}
}

[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
class DebuggerDisplayMethodTest
{
int someInt = 32;
int someInt2 = 43;

string GetDebuggerDisplay ()
{
return "First Int:" + someInt + " Second Int:" + someInt2;
}
}

class DebuggerCustomViewTest
{
public static void run()
{
var a = new WithDisplayString();
var b = new WithProxy();
var c = new DebuggerDisplayMethodTest();
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
}
}
}