diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs index 148fd03d37c84e..fc0dbf7aaf96a9 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs @@ -20,6 +20,7 @@ using Microsoft.CodeAnalysis.Debugging; using System.IO.Compression; using System.Reflection; +using System.Collections.Immutable; namespace Microsoft.WebAssembly.Diagnostics { @@ -524,39 +525,57 @@ internal class AssemblyInfo private Dictionary sourceLinkMappings = new Dictionary(); private readonly List sources = new List(); internal string Url { get; } + //The caller must keep the PEReader alive and undisposed throughout the lifetime of the metadata reader + internal PEReader peReader; internal MetadataReader asmMetadataReader { get; } internal MetadataReader pdbMetadataReader { get; set; } - internal List enCMemoryStream = new List(); internal List enCMetadataReader = new List(); - internal PEReader peReader; - internal MemoryStream asmStream; - internal MemoryStream pdbStream; public int DebugId { get; set; } - + internal int PdbAge { get; } + internal System.Guid PdbGuid { get; } + internal string PdbName { get; } + internal bool PdbInformationAvailable { get; } public bool TriedToLoadSymbolsOnDemand { get; set; } - public unsafe AssemblyInfo(string url, byte[] assembly, byte[] pdb) + public unsafe AssemblyInfo(MonoProxy monoProxy, SessionId sessionId, string url, byte[] assembly, byte[] pdb, CancellationToken token) { this.id = Interlocked.Increment(ref next_id); - asmStream = new MemoryStream(assembly); + using var asmStream = new MemoryStream(assembly); peReader = new PEReader(asmStream); + var entries = peReader.ReadDebugDirectory(); + if (entries.Length > 0) + { + var codeView = entries[0]; + CodeViewDebugDirectoryData codeViewData = peReader.ReadCodeViewDebugDirectoryData(codeView); + PdbAge = codeViewData.Age; + PdbGuid = codeViewData.Guid; + PdbName = codeViewData.Path; + PdbInformationAvailable = true; + } asmMetadataReader = PEReaderExtensions.GetMetadataReader(peReader); + var asmDef = asmMetadataReader.GetAssemblyDefinition(); + Name = asmDef.GetAssemblyName().Name + ".dll"; if (pdb != null) { - pdbStream = new MemoryStream(pdb); - pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader(); + var pdbStream = new MemoryStream(pdb); + try + { + // MetadataReaderProvider.FromPortablePdbStream takes ownership of the stream + pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader(); + } + catch (BadImageFormatException) + { + monoProxy.SendLog(sessionId, $"Warning: Unable to read debug information of: {Name} (use DebugType=Portable/Embedded)", token); + } } else { - var entries = peReader.ReadDebugDirectory(); var embeddedPdbEntry = entries.FirstOrDefault(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); if (embeddedPdbEntry.DataSize != 0) { pdbMetadataReader = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry).GetMetadataReader(); } } - Name = asmMetadataReader.GetAssemblyDefinition().GetAssemblyName().Name + ".dll"; - AssemblyNameUnqualified = asmMetadataReader.GetAssemblyDefinition().GetAssemblyName().Name + ".dll"; Populate(); } @@ -566,8 +585,6 @@ public bool EnC(byte[] meta, byte[] pdb) MetadataReader asmMetadataReader = MetadataReaderProvider.FromMetadataStream(asmStream).GetMetadataReader(); var pdbStream = new MemoryStream(pdb); MetadataReader pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader(); - enCMemoryStream.Add(asmStream); - enCMemoryStream.Add(pdbStream); enCMetadataReader.Add(asmMetadataReader); enCMetadataReader.Add(pdbMetadataReader); PopulateEnC(asmMetadataReader, pdbMetadataReader); @@ -728,7 +745,7 @@ public TypeInfo GetTypeByName(string name) internal void UpdatePdbInformation(Stream streamToReadFrom) { - pdbStream = new MemoryStream(); + var pdbStream = new MemoryStream(); streamToReadFrom.CopyTo(pdbStream); pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader(); } @@ -907,14 +924,16 @@ internal class DebugStore internal List assemblies = new List(); private readonly HttpClient client; private readonly ILogger logger; + private readonly MonoProxy monoProxy; - public DebugStore(ILogger logger, HttpClient client) + public DebugStore(MonoProxy monoProxy, ILogger logger, HttpClient client) { this.client = client; this.logger = logger; + this.monoProxy = monoProxy; } - public DebugStore(ILogger logger) : this(logger, new HttpClient()) + public DebugStore(MonoProxy monoProxy, ILogger logger) : this(monoProxy, logger, new HttpClient()) { } private class DebugItem @@ -933,12 +952,12 @@ public IEnumerable EnC(AssemblyInfo asm, byte[] meta_data, byte[] pd } } - public IEnumerable Add(string name, byte[] assembly_data, byte[] pdb_data) + public IEnumerable Add(SessionId id, string name, byte[] assembly_data, byte[] pdb_data, CancellationToken token) { AssemblyInfo assembly = null; try { - assembly = new AssemblyInfo(name, assembly_data, pdb_data); + assembly = new AssemblyInfo(monoProxy, id, name, assembly_data, pdb_data, token); } catch (Exception e) { @@ -949,7 +968,7 @@ public IEnumerable Add(string name, byte[] assembly_data, byte[] pdb if (assembly == null) yield break; - if (GetAssemblyByUnqualifiedName(assembly.AssemblyNameUnqualified) != null) + if (GetAssemblyByName(assembly.Name) != null) { logger.LogDebug($"Skipping adding {assembly.Name} into the debug store, as it already exists"); yield break; @@ -962,7 +981,7 @@ public IEnumerable Add(string name, byte[] assembly_data, byte[] pdb } } - public async IAsyncEnumerable Load(string[] loaded_files, [EnumeratorCancellation] CancellationToken token) + public async IAsyncEnumerable Load(SessionId id, string[] loaded_files, [EnumeratorCancellation] CancellationToken token) { var asm_files = new List(); var pdb_files = new List(); @@ -1001,7 +1020,7 @@ public async IAsyncEnumerable Load(string[] loaded_files, [Enumerato try { byte[][] bytes = await step.Data.ConfigureAwait(false); - assembly = new AssemblyInfo(step.Url, bytes[0], bytes[1]); + assembly = new AssemblyInfo(monoProxy, id, step.Url, bytes[0], bytes[1], token); } catch (Exception e) { @@ -1010,7 +1029,7 @@ public async IAsyncEnumerable Load(string[] loaded_files, [Enumerato if (assembly == null) continue; - if (GetAssemblyByUnqualifiedName(assembly.AssemblyNameUnqualified) != null) + if (GetAssemblyByName(assembly.Name) != null) { logger.LogDebug($"Skipping loading {assembly.Name} into the debug store, as it already exists"); continue; @@ -1028,7 +1047,6 @@ public async IAsyncEnumerable Load(string[] loaded_files, [Enumerato public AssemblyInfo GetAssemblyByName(string name) => assemblies.FirstOrDefault(a => a.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); - public AssemblyInfo GetAssemblyByUnqualifiedName(string name) => assemblies.FirstOrDefault(a => a.AssemblyNameUnqualified.Equals(name, StringComparison.InvariantCultureIgnoreCase)); /* V8 uses zero based indexing for both line and column. diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index f6689614aafd71..e0f796e09c5ad2 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -50,6 +50,34 @@ private bool UpdateContext(SessionId sessionId, ExecutionContext executionContex internal Task SendMonoCommand(SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand(id, "Runtime.evaluate", JObject.FromObject(cmd), token); + internal void SendLog(SessionId sessionId, string message, CancellationToken token) + { + if (!contexts.TryGetValue(sessionId, out ExecutionContext context)) + return; + /*var o = JObject.FromObject(new + { + entry = JObject.FromObject(new + { + source = "recommendation", + level = "warning", + text = message + }) + }); + SendEvent(id, "Log.enabled", null, token); + SendEvent(id, "Log.entryAdded", o, token);*/ + var o = JObject.FromObject(new + { + type = "warning", + args = new JArray(JObject.FromObject(new + { + type = "string", + value = message, + })), + executionContextId = context.Id + }); + SendEvent(sessionId, "Runtime.consoleAPICalled", o, token); + } + protected override async Task AcceptEvent(SessionId sessionId, string method, JObject args, CancellationToken token) { switch (method) @@ -936,21 +964,14 @@ internal async Task LoadSymbolsOnDemand(AssemblyInfo asm, int method ExecutionContext context = GetContext(sessionId); if (urlSymbolServerList.Count == 0) return null; - if (asm.TriedToLoadSymbolsOnDemand) + if (asm.TriedToLoadSymbolsOnDemand || !asm.PdbInformationAvailable) return null; asm.TriedToLoadSymbolsOnDemand = true; - var peReader = asm.peReader; - var entries = peReader.ReadDebugDirectory(); - var codeView = entries[0]; - var codeViewData = peReader.ReadCodeViewDebugDirectoryData(codeView); - int pdbAge = codeViewData.Age; - var pdbGuid = codeViewData.Guid; - string pdbName = codeViewData.Path; - pdbName = Path.GetFileName(pdbName); + var pdbName = Path.GetFileName(asm.PdbName); foreach (string urlSymbolServer in urlSymbolServerList) { - string downloadURL = $"{urlSymbolServer}/{pdbName}/{pdbGuid.ToString("N").ToUpper() + pdbAge}/{pdbName}"; + string downloadURL = $"{urlSymbolServer}/{pdbName}/{asm.PdbGuid.ToString("N").ToUpper() + asm.PdbAge}/{pdbName}"; try { @@ -1064,7 +1085,7 @@ private async Task OnAssemblyLoadedJSEvent(SessionId sessionId, JObject ev var store = await LoadStore(sessionId, token); var assembly_name = eventArgs?["assembly_name"]?.Value(); - if (store.GetAssemblyByUnqualifiedName(assembly_name) != null) + if (store.GetAssemblyByName(assembly_name) != null) { Log("debug", $"Got AssemblyLoaded event for {assembly_name}, but skipping it as it has already been loaded."); return true; @@ -1083,7 +1104,7 @@ private async Task OnAssemblyLoadedJSEvent(SessionId sessionId, JObject ev var pdb_data = string.IsNullOrEmpty(pdb_b64) ? null : Convert.FromBase64String(pdb_b64); var context = GetContext(sessionId); - foreach (var source in store.Add(assembly_name, assembly_data, pdb_data)) + foreach (var source in store.Add(sessionId, assembly_name, assembly_data, pdb_data, token)) { await OnSourceFileAdded(sessionId, source, context, token); } @@ -1211,7 +1232,7 @@ internal async Task LoadStore(SessionId sessionId, CancellationToken { ExecutionContext context = GetContext(sessionId); - if (Interlocked.CompareExchange(ref context.store, new DebugStore(logger), null) != null) + if (Interlocked.CompareExchange(ref context.store, new DebugStore(this, logger), null) != null) return await context.Source.Task; try @@ -1225,7 +1246,7 @@ internal async Task LoadStore(SessionId sessionId, CancellationToken } await - foreach (SourceFile source in context.store.Load(loaded_files, token).WithCancellation(token)) + foreach (SourceFile source in context.store.Load(sessionId, loaded_files, token).WithCancellation(token)) { await OnSourceFileAdded(sessionId, source, context, token); } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs index d6b260375c1f0e..6025d684659359 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs @@ -901,6 +901,35 @@ await EvaluateAndCheck( } ); } + + [Fact] + public async Task InspectLocalsUsingClassFromLibraryUsingDebugTypeFull() + { + byte[] bytes = File.ReadAllBytes(Path.Combine(DebuggerTestAppPath, "debugger-test-with-full-debug-type.dll")); + string asm_base64 = Convert.ToBase64String(bytes); + + string pdb_base64 = null; + bytes = File.ReadAllBytes(Path.Combine(DebuggerTestAppPath, "debugger-test-with-full-debug-type.pdb")); + pdb_base64 = Convert.ToBase64String(bytes); + + var expression = $"{{ let asm_b64 = '{asm_base64}'; let pdb_b64 = '{pdb_base64}'; invoke_static_method('[debugger-test] DebugTypeFull:CallToEvaluateLocal', asm_b64, pdb_b64); }}"; + + await EvaluateAndCheck( + "window.setTimeout(function() {" + expression + "; }, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", 860, 8, + "CallToEvaluateLocal", + wait_for_event_fn: async (pause_location) => + { + var a_props = await GetObjectOnFrame(pause_location["callFrames"][0], "a"); + await CheckProps(a_props, new + { + a = TNumber(10), + b = TNumber(20), + c = TNumber(30) + }, "a"); + } + ); + } //TODO add tests covering basic stepping behavior as step in/out/over } } diff --git a/src/mono/wasm/debugger/tests/debugger-test-with-full-debug-type/debugger-test-with-full-debug-type.csproj b/src/mono/wasm/debugger/tests/debugger-test-with-full-debug-type/debugger-test-with-full-debug-type.csproj new file mode 100644 index 00000000000000..7746f400741700 --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-test-with-full-debug-type/debugger-test-with-full-debug-type.csproj @@ -0,0 +1,5 @@ + + + full + + diff --git a/src/mono/wasm/debugger/tests/debugger-test-with-full-debug-type/test.cs b/src/mono/wasm/debugger/tests/debugger-test-with-full-debug-type/test.cs new file mode 100644 index 00000000000000..3b9ab8aecbbd1a --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-test-with-full-debug-type/test.cs @@ -0,0 +1,20 @@ +using System; + +namespace DebuggerTests +{ + public class ClassToInspectWithDebugTypeFull + { + int a; + int b; + int c; + public ClassToInspectWithDebugTypeFull() + { + a = 10; + b = 20; + c = 30; + Console.WriteLine(a); + Console.WriteLine(b); + Console.WriteLine(c); + } + } +} diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs index 813102bbbc047d..3d3502a83d4ade 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs @@ -849,3 +849,15 @@ public static void RunDebuggerBreak() VisibleMethodDebuggerBreak(); } } + +public class DebugTypeFull +{ + public static void CallToEvaluateLocal(string asm_base64, string pdb_base64) + { + var asm = System.Reflection.Assembly.LoadFrom("debugger-test-with-full-debug-type.dll"); + var myType = asm.GetType("DebuggerTests.ClassToInspectWithDebugTypeFull"); + var myMethod = myType.GetConstructor(new Type[] { }); + var a = myMethod.Invoke(new object[]{}); + System.Diagnostics.Debugger.Break(); + } +} diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj index c3cc1800aad87e..379d4d16faa1df 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj @@ -22,6 +22,7 @@ + @@ -38,6 +39,7 @@ +