diff --git a/src/mono/sample/wasm/browser/Program.cs b/src/mono/sample/wasm/browser/Program.cs index 743f481896fcb5..f87cd9103c749b 100644 --- a/src/mono/sample/wasm/browser/Program.cs +++ b/src/mono/sample/wasm/browser/Program.cs @@ -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!"); @@ -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; } } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs index fb4eda76ca2454..de40842443c2a8 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs @@ -322,5 +322,16 @@ internal class PerScopeCache { public Dictionary Locals { get; } = new Dictionary(); public Dictionary MemberReferences { get; } = new Dictionary(); + public Dictionary ObjectFields { get; } = new Dictionary(); + public PerScopeCache(JArray objectValues) + { + foreach (var objectValue in objectValues) + { + ObjectFields[objectValue["name"].Value()] = objectValue.Value(); + } + } + public PerScopeCache() + { + } } } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs index 58374c43565c0e..e551c16dfe5c37 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -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); diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs index cd21ac64190944..3e791f2f3e057d 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs @@ -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) { @@ -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 GetValueFromObject(JToken objRet, CancellationToken token) { if (objRet["value"]?["className"]?.Value() == "System.Exception") @@ -76,6 +88,11 @@ public async Task 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(); @@ -96,12 +113,12 @@ public async Task 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)) { @@ -145,6 +162,12 @@ public async Task 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(), out DotnetObjectId objectId); diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 1872ec1d2a93e8..4dc7c374268c4b 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -29,7 +29,7 @@ internal class MonoProxy : DevToolsProxy public MonoProxy(ILoggerFactory loggerFactory, IList urlSymbolServerList) : base(loggerFactory) { this.urlSymbolServerList = urlSymbolServerList ?? new List(); - SdbHelper = new MonoSDBHelper(this); + SdbHelper = new MonoSDBHelper(this, logger); } internal ExecutionContext GetContext(SessionId sessionId) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index d12533c1e9ffbd..645a6a4ac2a2ca 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -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() @@ -946,6 +949,7 @@ public async Task> GetTypeFields(SessionId sessionId, int t } return ret; } + public string ReplaceCommonClassNames(string className) { className = className.Replace("System.String", "string"); @@ -957,6 +961,88 @@ public string ReplaceCommonClassNames(string className) className = className.Replace("System.Byte", "byte"); return className; } + + public async Task 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(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(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(), 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().Equals("Value")); + try { + ExecutionContext context = proxy.GetContext(sessionId); + var objectValues = await GetObjectValues(sessionId, objectId, true, false, false, false, token); + + var thisObj = CreateJObject("", "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(); + //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(); + } + 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 GetTypeName(SessionId sessionId, int type_id, CancellationToken token) { var commandParams = new MemoryStream(); @@ -1043,7 +1129,7 @@ public async Task 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(sessionId, CmdType.GetMethodsByNameFlags, commandParams, token); var nMethods = retDebuggerCmdReader.ReadInt32(); @@ -1299,7 +1385,12 @@ public async Task 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) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs new file mode 100644 index 00000000000000..c848c03c303b70 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs @@ -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(), + bp.Value["locations"][0]["columnNumber"].Value(), + "run"); + + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + CheckObject(locals, "a", "DebuggerTests.WithDisplayString", description:"Some one Value 2 End"); + CheckObject(locals, "c", "DebuggerTests.DebuggerDisplayMethodTest", description: "First Int:32 Second Int:43"); + } + } +} diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-custom-view-test.cs b/src/mono/wasm/debugger/tests/debugger-test/debugger-custom-view-test.cs new file mode 100644 index 00000000000000..2d97a9b64a37e2 --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-custom-view-test.cs @@ -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); + } + } +}