From f26b2cef98e050ebae5e1c118aa9d401674e545d Mon Sep 17 00:00:00 2001 From: Thays Grazia Date: Mon, 23 Jan 2023 19:43:32 -0300 Subject: [PATCH] [wasm][debugger] Implement support to symbolOptions from dap. (#79284) * Draft to implement support to symbolOptions from dap. * removing debugger.launch * Adding tests and fix compilation error * Adding test case. * Fix test cases, and implement support to PDBChecksum used on nuget.org to get symbols. * merge * Fixing tests. * Tests are timing out. * Apply suggestions from code review Co-authored-by: Larry Ewing Co-authored-by: Ankit Jain * adressing @radical comments. * Addressing @radical comment. * Addressing @radical comments. * Apply suggestions from code review Co-authored-by: Ankit Jain * Addressing @radical comments. * Addressing @radical comments Changing when the symbols from symbol server is loaded because it takes a long time to load, as there are a lot of assemblies loaded not found on symbol servers. * use MicrosoftCodeAnalysisCSharpVersion for scripting package. * Adding more tests as asked by @radical Removing timeout change as @radical has split it into 2 files Fixing test behavior, when justMyCode is disabled but the symbols are not loaded from symbol server. * [wasm] some cleanup - Don't call `UpdateSymbolStore` from `DebugStore..ctor` because that gets called multiple times in `LoadStore`, but only once instance gets used. - Use an isolated symbol cache path per test * remove debug spew * Addressing radical comment. Co-authored-by: Larry Ewing Co-authored-by: Ankit Jain --- eng/Versions.props | 4 + src/mono/mono/component/debugger-engine.c | 7 +- ...NETCore.BrowserDebugHost.Transport.pkgproj | 2 + .../wasm/debugger/BrowserDebugHost/Startup.cs | 3 +- .../BrowserDebugProxy.csproj | 7 +- .../debugger/BrowserDebugProxy/DebugStore.cs | 426 ++++++++++++------ .../BrowserDebugProxy/DebuggerProxy.cs | 4 +- .../BrowserDebugProxy/EvaluateExpression.cs | 6 +- .../Firefox/FirefoxMonoProxy.cs | 2 +- .../debugger/BrowserDebugProxy/MonoProxy.cs | 123 +++-- .../BrowserDebugProxy/MonoSDBHelper.cs | 13 - .../DebuggerTestSuite/ChromeProvider.cs | 2 +- .../DebuggerTestSuite/DebuggerTestBase.cs | 22 +- .../DebuggerTestSuite/EnvironmentVariables.cs | 2 + .../DebuggerTestSuite/SteppingTests.cs | 180 +++++++- .../DebuggerTestSuite/TestEnvironment.cs | 42 ++ .../tests/debugger-test/debugger-test.cs | 16 +- .../tests/debugger-test/debugger-test.csproj | 6 + 18 files changed, 635 insertions(+), 232 deletions(-) create mode 100644 src/mono/wasm/debugger/DebuggerTestSuite/TestEnvironment.cs diff --git a/eng/Versions.props b/eng/Versions.props index 3a5c1a55e9699..a57587c74a80d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -248,5 +248,9 @@ 1.0.0-alpha.1.23066.1 1.0.0-alpha.1.23066.1 1.0.0-alpha.1.23066.1 + + + 3.1.7 + 1.0.406601 diff --git a/src/mono/mono/component/debugger-engine.c b/src/mono/mono/component/debugger-engine.c index 50a3a0fec15c4..2a05aba440a07 100644 --- a/src/mono/mono/component/debugger-engine.c +++ b/src/mono/mono/component/debugger-engine.c @@ -975,10 +975,11 @@ mono_de_ss_update (SingleStepReq *req, MonoJitInfo *ji, SeqPoint *sp, void *tls, if (minfo) loc = mono_debug_method_lookup_location (minfo, sp->il_offset); - if (!loc) { - PRINT_DEBUG_MSG (1, "[%p] No line number info for il offset %x, continuing single stepping.\n", (gpointer) (gsize) mono_native_thread_id_get (), sp->il_offset); + if (!loc) { //we should not continue single stepping because the client side can have symbols loaded dynamically + PRINT_DEBUG_MSG (1, "[%p] No line number info for il offset %x, don't know if it's in the same line single stepping.\n", (gpointer) (gsize) mono_native_thread_id_get (), sp->il_offset); req->last_method = method; - hit = FALSE; + req->last_line = -1; + return hit; } else if (loc && method == req->last_method && loc->row == req->last_line) { int nframes; rt_callbacks.ss_calculate_framecount (tls, ctx, FALSE, NULL, &nframes); diff --git a/src/mono/nuget/Microsoft.NETCore.BrowserDebugHost.Transport/Microsoft.NETCore.BrowserDebugHost.Transport.pkgproj b/src/mono/nuget/Microsoft.NETCore.BrowserDebugHost.Transport/Microsoft.NETCore.BrowserDebugHost.Transport.pkgproj index 2119425f85f0c..8294b2cbd38dc 100644 --- a/src/mono/nuget/Microsoft.NETCore.BrowserDebugHost.Transport/Microsoft.NETCore.BrowserDebugHost.Transport.pkgproj +++ b/src/mono/nuget/Microsoft.NETCore.BrowserDebugHost.Transport/Microsoft.NETCore.BrowserDebugHost.Transport.pkgproj @@ -20,6 +20,8 @@ <_browserDebugHostFiles Include="$(ArtifactsDir)bin\BrowserDebugHost\$(TargetArchitecture)\$(Configuration)\Newtonsoft.Json.dll" /> <_browserDebugHostFiles Include="$(ArtifactsDir)bin\BrowserDebugHost\$(TargetArchitecture)\$(Configuration)\Microsoft.CodeAnalysis.CSharp.Scripting.dll" /> <_browserDebugHostFiles Include="$(ArtifactsDir)bin\BrowserDebugHost\$(TargetArchitecture)\$(Configuration)\Microsoft.CodeAnalysis.Scripting.dll" /> + <_browserDebugHostFiles Include="$(ArtifactsDir)bin\BrowserDebugHost\$(TargetArchitecture)\$(Configuration)\Microsoft.SymbolStore.dll" /> + <_browserDebugHostFiles Include="$(ArtifactsDir)bin\BrowserDebugHost\$(TargetArchitecture)\$(Configuration)\Microsoft.FileFormats.dll" /> diff --git a/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs b/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs index 9e092af92f639..a6856867b3ef5 100644 --- a/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs +++ b/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs @@ -203,8 +203,7 @@ async Task ConnectProxy(HttpContext context) try { var loggerFactory = context.RequestServices.GetService(); - context.Request.Query.TryGetValue("urlSymbolServer", out StringValues urlSymbolServerList); - var proxy = new DebuggerProxy(loggerFactory, urlSymbolServerList.ToList(), runtimeId, options: options); + var proxy = new DebuggerProxy(loggerFactory, runtimeId, options: options); System.Net.WebSockets.WebSocket ideSocket = await context.WebSockets.AcceptWebSocketAsync(); diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.csproj b/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.csproj index eb93b1a34e23f..22f0a963aecd9 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.csproj +++ b/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.csproj @@ -8,10 +8,11 @@ - - + + + - + diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs index d053921f05415..06ddcb08de677 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs @@ -21,6 +21,10 @@ using System.Reflection; using System.Diagnostics; using System.Text; +using Microsoft.SymbolStore; +using Microsoft.SymbolStore.SymbolStores; +using Microsoft.FileFormats.PE; +using Microsoft.Extensions.Primitives; using Microsoft.NET.WebAssembly.Webcil; namespace Microsoft.WebAssembly.Diagnostics @@ -324,7 +328,7 @@ public override bool Equals(object obj) internal sealed class MethodInfo { private MethodDefinition methodDef; - internal SourceFile Source { get; } + internal SourceFile Source { get; set; } public SourceId SourceId => Source.SourceId; @@ -333,8 +337,8 @@ internal sealed class MethodInfo public string Name { get; } public MethodDebugInformation DebugInformation; public MethodDefinitionHandle methodDefHandle; - private MetadataReader pdbMetadataReader; - private bool hasDebugInformation; + internal MetadataReader pdbMetadataReader; + internal bool hasDebugInformation; public SourceLocation StartLocation { get; set; } public SourceLocation EndLocation { get; set; } @@ -351,7 +355,7 @@ internal sealed class MethodInfo private ParameterInfo[] _parametersInfo; public int KickOffMethod { get; } internal bool IsCompilerGenerated { get; } - private readonly AsyncScopeDebugInformation[] _asyncScopes; + private AsyncScopeDebugInformation[] _asyncScopes { get; set; } public MethodInfo(AssemblyInfo assembly, string methodName, int methodToken, TypeInfo type, MethodAttributes attrs) { @@ -372,105 +376,49 @@ public MethodInfo(AssemblyInfo assembly, MethodDefinitionHandle methodDefHandle, this.Assembly = assembly; this.methodDef = asmMetadataReader.GetMethodDefinition(methodDefHandle); this.Attributes = methodDef.Attributes; - if (pdbMetadataReader != null && !methodDefHandle.ToDebugInformationHandle().IsNil) - { - this.DebugInformation = pdbMetadataReader.GetMethodDebugInformation(methodDefHandle.ToDebugInformationHandle()); - hasDebugInformation = true; - } this.Source = source; this.Token = token; this.methodDefHandle = methodDefHandle; this.Name = assembly.EnCGetString(methodDef.Name); this.pdbMetadataReader = pdbMetadataReader; + UpdatePdbInformation(methodDefHandle); if (hasDebugInformation && !DebugInformation.GetStateMachineKickoffMethod().IsNil) this.KickOffMethod = asmMetadataReader.GetRowNumber(DebugInformation.GetStateMachineKickoffMethod()); else this.KickOffMethod = -1; this.IsEnCMethod = false; this.TypeInfo = type; - if (HasSequencePoints && source != null) + DebuggerAttrInfo = new DebuggerAttributesInfo(); + foreach (var cattr in methodDef.GetCustomAttributes()) { - var sps = DebugInformation.GetSequencePoints(); - SequencePoint start = sps.First(); - SequencePoint end = sps.First(); - source.BreakableLines.Add(start.StartLine); - foreach (SequencePoint sp in sps) + var ctorHandle = asmMetadataReader.GetCustomAttribute(cattr).Constructor; + if (ctorHandle.Kind == HandleKind.MemberReference) { - if (source.BreakableLines.Last() != sp.StartLine) - source.BreakableLines.Add(sp.StartLine); - - if (sp.IsHidden) - continue; - - if (sp.StartLine < start.StartLine) - start = sp; - else if (sp.StartLine == start.StartLine && sp.StartColumn < start.StartColumn) - start = sp; - - if (end.EndLine == SequencePoint.HiddenLine) - end = sp; - if (sp.EndLine > end.EndLine) - end = sp; - else if (sp.EndLine == end.EndLine && sp.EndColumn > end.EndColumn) - end = sp; - } - - StartLocation = new SourceLocation(this, start); - EndLocation = new SourceLocation(this, end); - - DebuggerAttrInfo = new DebuggerAttributesInfo(); - foreach (var cattr in methodDef.GetCustomAttributes()) - { - var ctorHandle = asmMetadataReader.GetCustomAttribute(cattr).Constructor; - if (ctorHandle.Kind == HandleKind.MemberReference) + var container = asmMetadataReader.GetMemberReference((MemberReferenceHandle)ctorHandle).Parent; + var name = assembly.EnCGetString(asmMetadataReader.GetTypeReference((TypeReferenceHandle)container).Name); + switch (name) { - var container = asmMetadataReader.GetMemberReference((MemberReferenceHandle)ctorHandle).Parent; - var name = assembly.EnCGetString(asmMetadataReader.GetTypeReference((TypeReferenceHandle)container).Name); - switch (name) - { - case "DebuggerHiddenAttribute": - DebuggerAttrInfo.HasDebuggerHidden = true; - break; - case "DebuggerStepThroughAttribute": - DebuggerAttrInfo.HasStepThrough = true; - break; - case "DebuggerNonUserCodeAttribute": - DebuggerAttrInfo.HasNonUserCode = true; - break; - case "DebuggerStepperBoundaryAttribute": - DebuggerAttrInfo.HasStepperBoundary = true; - break; - case nameof(CompilerGeneratedAttribute): - IsCompilerGenerated = true; - break; - } - - } - } - DebuggerAttrInfo.ClearInsignificantAttrFlags(); - } - if (pdbMetadataReader != null) - { - localScopes = pdbMetadataReader.GetLocalScopes(methodDefHandle); - byte[] scopeDebugInformation = - (from cdiHandle in pdbMetadataReader.GetCustomDebugInformation(methodDefHandle) - let cdi = pdbMetadataReader.GetCustomDebugInformation(cdiHandle) - where pdbMetadataReader.GetGuid(cdi.Kind) == PortableCustomDebugInfoKinds.StateMachineHoistedLocalScopes - select pdbMetadataReader.GetBlobBytes(cdi.Value)).FirstOrDefault(); - - if (scopeDebugInformation != null) - { - _asyncScopes = new AsyncScopeDebugInformation[scopeDebugInformation.Length / 8]; - for (int i = 0; i < _asyncScopes.Length; i++) - { - int scopeOffset = BitConverter.ToInt32(scopeDebugInformation, i * 8); - int scopeLen = BitConverter.ToInt32(scopeDebugInformation, (i * 8) + 4); - _asyncScopes[i] = new AsyncScopeDebugInformation(scopeOffset, scopeOffset + scopeLen); + case "DebuggerHiddenAttribute": + DebuggerAttrInfo.HasDebuggerHidden = true; + break; + case "DebuggerStepThroughAttribute": + DebuggerAttrInfo.HasStepThrough = true; + break; + case "DebuggerNonUserCodeAttribute": + DebuggerAttrInfo.HasNonUserCode = true; + break; + case "DebuggerStepperBoundaryAttribute": + DebuggerAttrInfo.HasStepperBoundary = true; + break; + case nameof(CompilerGeneratedAttribute): + IsCompilerGenerated = true; + break; } } - - _asyncScopes ??= Array.Empty(); } + if (!hasDebugInformation) + DebuggerAttrInfo.HasNonUserCode = true; + DebuggerAttrInfo.ClearInsignificantAttrFlags(); } public bool ContainsAsyncScope(int oneBasedIdx, int offset) @@ -512,24 +460,41 @@ public ParameterInfo[] GetParametersInfo() return paramsInfo; } - public void UpdateEnC(MetadataReader pdbMetadataReaderParm, int methodIdx) + public void UpdatePdbInformation(MethodDefinitionHandle methodDefHandleParm) { - this.DebugInformation = pdbMetadataReaderParm.GetMethodDebugInformation(MetadataTokens.MethodDebugInformationHandle(methodIdx)); - this.pdbMetadataReader = pdbMetadataReaderParm; - this.IsEnCMethod = true; - if (HasSequencePoints) + if (pdbMetadataReader == null || methodDefHandleParm.ToDebugInformationHandle().IsNil) + return; + DebugInformation = pdbMetadataReader.GetMethodDebugInformation(methodDefHandleParm.ToDebugInformationHandle()); + if (Source == null && !DebugInformation.Document.IsNil) + { + var document = pdbMetadataReader.GetDocument(DebugInformation.Document); + var documentName = pdbMetadataReader.GetString(document.Name); + Source = Assembly.GetOrAddSourceFile(DebugInformation.Document, documentName); + Source.AddMethod(this); + } + hasDebugInformation = true; + if (HasSequencePoints && Source != null) { var sps = DebugInformation.GetSequencePoints(); SequencePoint start = sps.First(); SequencePoint end = sps.First(); + Source.BreakableLines.Add(start.StartLine); foreach (SequencePoint sp in sps) { + if (Source.BreakableLines.Last() != sp.StartLine) + Source.BreakableLines.Add(sp.StartLine); + + if (sp.IsHidden) + continue; + if (sp.StartLine < start.StartLine) start = sp; else if (sp.StartLine == start.StartLine && sp.StartColumn < start.StartColumn) start = sp; + if (end.EndLine == SequencePoint.HiddenLine) + end = sp; if (sp.EndLine > end.EndLine) end = sp; else if (sp.EndLine == end.EndLine && sp.EndColumn > end.EndColumn) @@ -539,7 +504,34 @@ public void UpdateEnC(MetadataReader pdbMetadataReaderParm, int methodIdx) StartLocation = new SourceLocation(this, start); EndLocation = new SourceLocation(this, end); } - localScopes = pdbMetadataReader.GetLocalScopes(MetadataTokens.MethodDefinitionHandle(methodIdx)); + localScopes = pdbMetadataReader.GetLocalScopes(methodDefHandleParm); + + byte[] scopeDebugInformation = + (from cdiHandle in pdbMetadataReader.GetCustomDebugInformation(methodDefHandleParm) + let cdi = pdbMetadataReader.GetCustomDebugInformation(cdiHandle) + where pdbMetadataReader.GetGuid(cdi.Kind) == PortableCustomDebugInfoKinds.StateMachineHoistedLocalScopes + select pdbMetadataReader.GetBlobBytes(cdi.Value)).FirstOrDefault(); + + if (scopeDebugInformation != null) + { + _asyncScopes = new AsyncScopeDebugInformation[scopeDebugInformation.Length / 8]; + for (int i = 0; i < _asyncScopes.Length; i++) + { + int scopeOffset = BitConverter.ToInt32(scopeDebugInformation, i * 8); + int scopeLen = BitConverter.ToInt32(scopeDebugInformation, (i * 8) + 4); + _asyncScopes[i] = new AsyncScopeDebugInformation(scopeOffset, scopeOffset + scopeLen); + } + } + + _asyncScopes ??= Array.Empty(); + } + + public void UpdateEnC(MetadataReader pdbMetadataReaderParm, int methodIdx) + { + this.DebugInformation = pdbMetadataReaderParm.GetMethodDebugInformation(MetadataTokens.MethodDebugInformationHandle(methodIdx)); + this.pdbMetadataReader = pdbMetadataReaderParm; + this.IsEnCMethod = true; + UpdatePdbInformation(MetadataTokens.MethodDefinitionHandle(methodIdx)); } public SourceLocation GetLocationByIl(int pos) @@ -858,15 +850,18 @@ internal sealed class AssemblyInfo private readonly IDisposable peReaderOrWebcilReader; internal MetadataReader asmMetadataReader { get; } internal MetadataReader pdbMetadataReader { get; set; } + internal List> enCMetadataReader = new List>(); private int debugId; internal int PdbAge { get; } internal System.Guid PdbGuid { get; } + internal bool IsPortableCodeView { get; } internal string PdbName { get; } internal bool CodeViewInformationAvailable { get; } public bool TriedToLoadSymbolsOnDemand { get; set; } private readonly Dictionary _documentIdToSourceFileTable = new Dictionary(); + public PdbChecksum[] PdbChecksums { get; } public static AssemblyInfo FromBytes(MonoProxy monoProxy, SessionId sessionId, byte[] assembly, byte[] pdb, ILogger logger, CancellationToken token) { @@ -904,12 +899,20 @@ private static AssemblyInfo FromPEReader(MonoProxy monoProxy, SessionId sessionI { var entries = peReader.ReadDebugDirectory(); CodeViewDebugDirectoryData? codeViewData = null; - if (entries.Length > 0) + var isPortableCodeView = false; + List pdbChecksums = new(); + foreach (var entry in peReader.ReadDebugDirectory()) { - var codeView = entries[0]; - if (codeView.Type == DebugDirectoryEntryType.CodeView) + if (entry.Type == DebugDirectoryEntryType.CodeView) + { + codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); + if (entry.IsPortableCodeView) + isPortableCodeView = true; + } + if (entry.Type == DebugDirectoryEntryType.PdbChecksum) { - codeViewData = peReader.ReadCodeViewDebugDirectoryData(codeView); + var checksum = peReader.ReadPdbChecksumDebugDirectoryData(entry); + pdbChecksums.Add(new PdbChecksum(checksum.AlgorithmName, checksum.Checksum.ToArray())); } } var asmMetadataReader = PEReaderExtensions.GetMetadataReader(peReader); @@ -938,7 +941,7 @@ private static AssemblyInfo FromPEReader(MonoProxy monoProxy, SessionId sessionI } } - var assemblyInfo = new AssemblyInfo(peReader, name, asmMetadataReader, codeViewData, pdbMetadataReader, logger); + var assemblyInfo = new AssemblyInfo(peReader, name, asmMetadataReader, codeViewData, pdbChecksums.ToArray(), isPortableCodeView, pdbMetadataReader, logger); return assemblyInfo; } @@ -946,12 +949,16 @@ private static AssemblyInfo FromWebcilReader(MonoProxy monoProxy, SessionId sess { var entries = wcReader.ReadDebugDirectory(); CodeViewDebugDirectoryData? codeViewData = null; - if (entries.Length > 0) + var isPortableCodeView = false; + List pdbChecksums = new(); + foreach (var entry in entries) { var codeView = entries[0]; if (codeView.Type == DebugDirectoryEntryType.CodeView) { codeViewData = wcReader.ReadCodeViewDebugDirectoryData(codeView); + if (codeView.IsPortableCodeView) + isPortableCodeView = true; } } var asmMetadataReader = wcReader.GetMetadataReader(); @@ -980,7 +987,7 @@ private static AssemblyInfo FromWebcilReader(MonoProxy monoProxy, SessionId sess } } - var assemblyInfo = new AssemblyInfo(wcReader, name, asmMetadataReader, codeViewData, pdbMetadataReader, logger); + var assemblyInfo = new AssemblyInfo(wcReader, name, asmMetadataReader, codeViewData, pdbChecksums.ToArray(), isPortableCodeView, pdbMetadataReader, logger); return assemblyInfo; } @@ -990,7 +997,7 @@ private static string ReadAssemblyName(MetadataReader asmMetadataReader) return asmDef.GetAssemblyName().Name + ".dll"; } - private unsafe AssemblyInfo(IDisposable owningReader, string name, MetadataReader asmMetadataReader, CodeViewDebugDirectoryData? codeViewData, MetadataReader pdbMetadataReader, ILogger logger) + private unsafe AssemblyInfo(IDisposable owningReader, string name, MetadataReader asmMetadataReader, CodeViewDebugDirectoryData? codeViewData, PdbChecksum[] pdbChecksums, bool isPortableCodeView, MetadataReader pdbMetadataReader, ILogger logger) : this(logger) { peReaderOrWebcilReader = owningReader; @@ -1001,6 +1008,8 @@ private unsafe AssemblyInfo(IDisposable owningReader, string name, MetadataReade PdbName = codeViewData.Value.Path; CodeViewInformationAvailable = true; } + IsPortableCodeView = isPortableCodeView; + PdbChecksums = pdbChecksums; this.asmMetadataReader = asmMetadataReader; Name = name; logger.LogTrace($"Info: loading AssemblyInfo with name {Name}"); @@ -1138,12 +1147,12 @@ private void PopulateEnC(MonoSDBHelper sdbAgent, MetadataReader asmMetadataReade } } } - private SourceFile GetOrAddSourceFile(DocumentHandle doc, string documentName) + public SourceFile GetOrAddSourceFile(DocumentHandle doc, string documentName) { if (_documentIdToSourceFileTable.TryGetValue(documentName.GetHashCode(), out SourceFile source)) return source; - var src = new SourceFile(this, _documentIdToSourceFileTable.Count, doc, GetSourceLinkUrl(documentName), documentName); + var src = new SourceFile(this, _documentIdToSourceFileTable.Count, doc, documentName, sourceLinkMappings); _documentIdToSourceFileTable[documentName.GetHashCode()] = src; return src; } @@ -1207,32 +1216,6 @@ where pdbMetadataReader.GetGuid(cdi.Kind) == PortableCustomDebugInfoKinds.Source } } - private Uri GetSourceLinkUrl(string document) - { - if (sourceLinkMappings.TryGetValue(document, out string url)) - return new Uri(url); - - foreach (KeyValuePair sourceLinkDocument in sourceLinkMappings) - { - string key = sourceLinkDocument.Key; - - if (!key.EndsWith("*")) - { - continue; - } - - string keyTrim = key.TrimEnd('*'); - - if (document.StartsWith(keyTrim, StringComparison.OrdinalIgnoreCase)) - { - string docUrlPart = document.Replace(keyTrim, ""); - return new Uri(sourceLinkDocument.Value.TrimEnd('*') + docUrlPart); - } - } - - return null; - } - public TypeInfo CreateTypeInfo(TypeDefinitionHandle typeHandle, TypeDefinition type) { var typeInfo = new TypeInfo(this, typeHandle, type, asmMetadataReader, logger); @@ -1278,13 +1261,40 @@ public TypeInfo GetTypeByName(string name) return res; } - internal void UpdatePdbInformation(Stream streamToReadFrom) + internal async Task LoadPDBFromSymbolServer(DebugStore debugStore, CancellationToken token) { - var pdbStream = new MemoryStream(); - streamToReadFrom.CopyTo(pdbStream); - pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader(); + try + { + if (TriedToLoadSymbolsOnDemand) + return; + var pdbName = Path.GetFileName(PdbName); + var pdbGuid = PdbGuid.ToString("N").ToUpperInvariant() + (IsPortableCodeView ? "FFFFFFFF" : PdbAge); + var key = $"{pdbName}/{pdbGuid}/{pdbName}"; + SymbolStoreFile file = await debugStore.symbolStore.GetFile(new SymbolStoreKey(key, PdbName, false, PdbChecksums), token); + TriedToLoadSymbolsOnDemand = true; + if (file == null) + return; + var pdbStream = new MemoryStream(); + file.Stream.Position = 0; + await file.Stream.CopyToAsync(pdbStream, token); + pdbStream.Position = 0; + pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader(); + if (pdbMetadataReader == null) + return; + ProcessSourceLink(); + foreach (var method in this.Methods) + { + method.Value.pdbMetadataReader = pdbMetadataReader; + method.Value.UpdatePdbInformation(method.Value.methodDefHandle); + } + } + catch (Exception ex) + { + logger.LogError($"Failed to load symbols from symbol server. ({ex.Message})"); + } } - } + +} internal sealed class SourceFile { private static readonly Regex regexForEscapeFileName = new(@"([:/])", RegexOptions.Compiled); @@ -1299,17 +1309,19 @@ internal sealed class SourceFile public string DotNetUrlEscaped { get; init; } public Uri Url { get; init; } - public Uri SourceLinkUri { get; init; } + public Uri SourceLinkUri { get; set; } public int Id { get; } public string AssemblyName => assembly.Name; public SourceId SourceId => new SourceId(assembly.Id, this.Id); public IEnumerable Methods => this.methods.Values; + private static SHA256 _sha256 = System.Security.Cryptography.SHA256.Create(); + private string _relativePath; - internal SourceFile(AssemblyInfo assembly, int id, DocumentHandle docHandle, Uri sourceLinkUri, string documentName) + internal SourceFile(AssemblyInfo assembly, int id, DocumentHandle docHandle, string documentName, Dictionary sourceLinkMappings) { this.methods = new Dictionary(); - this.SourceLinkUri = sourceLinkUri; + GetSourceLinkUrl(documentName, sourceLinkMappings); this.assembly = assembly; this.Id = id; this.doc = assembly.pdbMetadataReader.GetDocument(docHandle); @@ -1321,7 +1333,66 @@ internal SourceFile(AssemblyInfo assembly, int id, DocumentHandle docHandle, Uri string escapedDocumentName = EscapePathForUri(documentName.Replace("\\", "/")); this.FileUriEscaped = $"file://{(OperatingSystem.IsWindows() ? "/" : "")}{escapedDocumentName}"; this.DotNetUrlEscaped = $"dotnet://{assembly.Name}/{escapedDocumentName}"; - this.Url = new Uri(File.Exists(documentName) ? FileUriEscaped : DotNetUrlEscaped, UriKind.Absolute); + if (!File.Exists(documentName) && SourceLinkUri != null) + { + string sourceLinkCachedPathPartial = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SourceServer", GetHashOfString(SourceLinkUri.AbsoluteUri)); + string sourceLinkCachedPath = Path.Combine(sourceLinkCachedPathPartial, _relativePath); + if (File.Exists(sourceLinkCachedPath)) //first try to find on cache using relativePath as it's done by VS while debugging + { + this.FilePath = sourceLinkCachedPath; + escapedDocumentName = EscapePathForUri(this.FilePath.Replace("\\", "/")); + } + else + { + sourceLinkCachedPath = Path.Combine(sourceLinkCachedPathPartial, Path.GetFileName(_relativePath)); + if (File.Exists(sourceLinkCachedPath)) //second try to find on cache without relativePath as it's done by VS when using "Go To Definition (F12)" + { + this.FilePath = sourceLinkCachedPath; + escapedDocumentName = EscapePathForUri(this.FilePath.Replace("\\", "/")); + } + } + this.FileUriEscaped = $"file://{(OperatingSystem.IsWindows() ? "/" : "")}{escapedDocumentName}"; + } + this.Url = new Uri(File.Exists(this.FilePath) ? FileUriEscaped : DotNetUrlEscaped, UriKind.Absolute); + } + + private void GetSourceLinkUrl(string document, Dictionary sourceLinkMappings) + { + if (sourceLinkMappings.TryGetValue(document, out string url)) + { + SourceLinkUri = new Uri(url); + return; + } + + foreach (KeyValuePair sourceLinkDocument in sourceLinkMappings) + { + string key = sourceLinkDocument.Key; + + if (!key.EndsWith("*", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string keyTrim = key.TrimEnd('*'); + + if (document.StartsWith(keyTrim, StringComparison.OrdinalIgnoreCase)) + { + _relativePath = document.Replace(keyTrim, ""); + SourceLinkUri = new Uri(sourceLinkDocument.Value.TrimEnd('*') + _relativePath); + return; + } + } + } + + private static string GetHashOfString(string str) + { + byte[] bytes = _sha256.ComputeHash(UnicodeEncoding.Unicode.GetBytes(str)); + StringBuilder builder = new StringBuilder(bytes.Length*2); + foreach (byte b in bytes) + { + builder.Append(b.ToString("x2")); + } + return builder.ToString(); } private static string EscapePathForUri(string path) @@ -1464,12 +1535,18 @@ internal sealed class DebugStore { internal List assemblies = new List(); private readonly ILogger logger; - private readonly MonoProxy monoProxy; + internal readonly MonoProxy monoProxy; + private readonly ITracer _tracer; + internal Microsoft.SymbolStore.SymbolStores.SymbolStore symbolStore; + // The constructor can get invoked multiple times, but only *one* of + // the instances will be used. + // So, keep this light, and repeatable public DebugStore(MonoProxy monoProxy, ILogger logger) { this.logger = logger; this.monoProxy = monoProxy; + this._tracer = new Tracer(logger); } private sealed class DebugItem @@ -1774,5 +1851,78 @@ static List FindMethodsContainingLine(SourceFile sourceFile, int lin } public string ToUrl(SourceLocation location) => location != null ? GetFileById(location.Id).Url.OriginalString : ""; + + internal async Task ReloadAllPDBsFromSymbolServersAndSendSources(MonoProxy monoProxy, SessionId id, ExecutionContext context, CancellationToken token) + { + if (symbolStore == null) + return; + monoProxy.SendLog(id, "Loading symbols from symbol servers.", token); + foreach (var asm in assemblies.Where(asm => asm.pdbMetadataReader == null)) + { + asm.TriedToLoadSymbolsOnDemand = false; //force to load again because added another symbol server + await asm.LoadPDBFromSymbolServer(this, token); + foreach (var source in asm.Sources) + await monoProxy.OnSourceFileAdded(id, source, context, token); + } + monoProxy.SendLog(id, "Symbols from symbol servers completely loaded.", token); + } + + internal void UpdateSymbolStore(List urlSymbolServerList, string cachePathSymbolServer) + { + symbolStore = null; + foreach (var urlServer in urlSymbolServerList) + { + if (string.IsNullOrEmpty(urlServer)) + continue; + try + { + symbolStore = new HttpSymbolStore(_tracer, symbolStore, new Uri($"{urlServer}/"), null); + } + catch (Exception ex) + { + logger.LogError($"Failed to create HttpSymbolStore for this URL - {urlServer} - {ex.Message}"); + } + } + if (!string.IsNullOrEmpty(cachePathSymbolServer)) + { + try + { + symbolStore = new CacheSymbolStore(_tracer, symbolStore, cachePathSymbolServer); + } + catch (Exception ex) + { + logger.LogError($"Failed to create CacheSymbolStore for this path - {cachePathSymbolServer} - {ex.Message}"); + } + } + } + public sealed class Tracer : ITracer + { + private readonly ILogger _logger; + + public Tracer(ILogger logger) + { + this._logger = logger; + } + + public void WriteLine(string message) => _logger.LogTrace(message); + + public void WriteLine(string format, params object[] arguments) => _logger.LogTrace(format, arguments); + + public void Information(string message) => _logger.LogTrace(message); + + public void Information(string format, params object[] arguments) => _logger.LogTrace(format, arguments); + + public void Warning(string message) => _logger.LogTrace(message); + + public void Warning(string format, params object[] arguments) => _logger.LogTrace(format, arguments); + + public void Error(string message) => _logger.LogTrace(message); + + public void Error(string format, params object[] arguments) => _logger.LogTrace(format, arguments); + + public void Verbose(string message) => _logger.LogTrace(message); + + public void Verbose(string format, params object[] arguments) => _logger.LogTrace(format, arguments); + } } } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs index c4e7c5ef0cc76..5bac4e3d919b7 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs @@ -18,10 +18,10 @@ public class DebuggerProxy : DebuggerProxyBase { internal MonoProxy MonoProxy { get; } - public DebuggerProxy(ILoggerFactory loggerFactory, IList urlSymbolServerList, int runtimeId = 0, string loggerId = "", ProxyOptions options = null) + public DebuggerProxy(ILoggerFactory loggerFactory, int runtimeId = 0, string loggerId = "", ProxyOptions options = null) { string suffix = loggerId.Length > 0 ? $"-{loggerId}" : string.Empty; - MonoProxy = new MonoProxy(loggerFactory.CreateLogger($"DevToolsProxy{suffix}"), urlSymbolServerList, runtimeId, loggerId, options); + MonoProxy = new MonoProxy(loggerFactory.CreateLogger($"DevToolsProxy{suffix}"), runtimeId, loggerId, options); } public Task Run(Uri browserUri, WebSocket ideSocket, CancellationTokenSource cts) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs index 43565a2d623fe..829dc3a8d4f7e 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -58,7 +58,7 @@ public override void Visit(SyntaxNode node) if (visitCount == 0) { if (node is MemberAccessExpressionSyntax maes - && node.Kind() == SyntaxKind.SimpleMemberAccessExpression + && node.IsKind(SyntaxKind.SimpleMemberAccessExpression) && !(node.Parent is MemberAccessExpressionSyntax) && !(node.Parent is InvocationExpressionSyntax) && !(node.Parent is ElementAccessExpressionSyntax)) @@ -401,7 +401,7 @@ internal static async Task CompileAndRunTheExpression( // this fails with `"a)"` // because the code becomes: return (a)); // and the returned expression from GetExpressionFromSyntaxTree is `a`! - if (expressionTree.Kind() == SyntaxKind.IdentifierName || expressionTree.Kind() == SyntaxKind.ThisExpression) + if (expressionTree.IsKind(SyntaxKind.IdentifierName) || expressionTree.IsKind(SyntaxKind.ThisExpression)) { string varName = expressionTree.ToString(); JObject value = await resolver.Resolve(varName, token); @@ -416,7 +416,7 @@ internal static async Task CompileAndRunTheExpression( syntaxTree = replacer.ReplaceVars(syntaxTree, memberAccessValues, identifierValues, null, null); // eg. "this.dateTime", " dateTime.TimeOfDay" - if (expressionTree.Kind() == SyntaxKind.SimpleMemberAccessExpression && replacer.memberAccesses.Count == 1) + if (expressionTree.IsKind(SyntaxKind.SimpleMemberAccessExpression) && replacer.memberAccesses.Count == 1) { return memberAccessValues[0]; } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs index 3d811d85f0d6e..267e9bc0dd004 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs @@ -17,7 +17,7 @@ namespace Microsoft.WebAssembly.Diagnostics; internal sealed class FirefoxMonoProxy : MonoProxy { - public FirefoxMonoProxy(ILogger logger, string loggerId = null, ProxyOptions options = null) : base(logger, null, loggerId: loggerId, options: options) + public FirefoxMonoProxy(ILogger logger, string loggerId = null, ProxyOptions options = null) : base(logger, loggerId: loggerId, options: options) { } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index d0f68b017240a..3733c6a355039 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -18,7 +18,8 @@ namespace Microsoft.WebAssembly.Diagnostics { internal class MonoProxy : DevToolsProxy { - private IList urlSymbolServerList; + internal List UrlSymbolServerList { get; private set; } + internal string CachePathSymbolServer { get; private set; } private HashSet sessions = new HashSet(); private static readonly string[] s_executionContextIndependentCDPCommandNames = { "DotnetDebugger.setDebuggerProperty", "DotnetDebugger.runTests" }; protected Dictionary contexts = new Dictionary(); @@ -32,9 +33,9 @@ internal class MonoProxy : DevToolsProxy protected readonly ProxyOptions _options; - public MonoProxy(ILogger logger, IList urlSymbolServerList, int runtimeId = 0, string loggerId = "", ProxyOptions options = null) : base(logger, loggerId) + public MonoProxy(ILogger logger, int runtimeId = 0, string loggerId = "", ProxyOptions options = null) : base(logger, loggerId) { - this.urlSymbolServerList = urlSymbolServerList ?? new List(); + UrlSymbolServerList = new List(); RuntimeId = runtimeId; _options = options; _defaultPauseOnExceptions = PauseOnExceptionsKind.Unset; @@ -76,10 +77,10 @@ internal void SendLog(SessionId sessionId, string message, CancellationToken tok { type, args = new JArray(JObject.FromObject(new - { - type = "string", - value = message, - })), + { + type = "string", + value = message, + })), executionContextId = context.Id }); SendEvent(sessionId, "Runtime.consoleAPICalled", o, token); @@ -143,7 +144,7 @@ protected override async Task AcceptEvent(SessionId sessionId, JObject par bool? is_default = aux_data["isDefault"]?.Value(); if (is_default == true) { - await OnDefaultContext(sessionId, new ExecutionContext(new MonoSDBHelper (this, logger, sessionId), id, aux_data, _defaultPauseOnExceptions), token); + await OnDefaultContext(sessionId, new ExecutionContext(new MonoSDBHelper(this, logger, sessionId), id, aux_data, _defaultPauseOnExceptions), token); } } return true; @@ -170,6 +171,8 @@ protected override async Task AcceptEvent(SessionId sessionId, JObject par { await RuntimeReady(sessionId, token); await SendResume(sessionId, token); + if (!JustMyCode) + await ReloadSymbolsFromSymbolServer(sessionId, GetContext(sessionId), token); return true; } case "mono_wasm_fire_debugger_agent_message": @@ -240,7 +243,7 @@ protected override async Task AcceptCommand(MessageId id, JObject parms, C if (!contexts.TryGetValue(id, out ExecutionContext context) && !s_executionContextIndependentCDPCommandNames.Contains(method)) { - if (method == "Debugger.setPauseOnExceptions") + if (method == "Debugger.setPauseOnExceptions") { string state = args["state"].Value(); var pauseOnException = GetPauseOnExceptionsStatusFromString(state); @@ -505,11 +508,11 @@ protected override async Task AcceptCommand(MessageId id, JObject parms, C switch (property.Key) { case "JustMyCodeStepping": - SetJustMyCode(id, (bool) property.Value, token); - break; + await SetJustMyCode(id, (bool)property.Value, context, token); + break; default: logger.LogDebug($"DotnetDebugger.setDebuggerProperty failed for {property.Key} with value {property.Value}"); - break; + break; } } return true; @@ -534,11 +537,21 @@ protected override async Task AcceptCommand(MessageId id, JObject parms, C SendResponse(id, Result.Err("ApplyUpdate failed."), token); return true; } - case "DotnetDebugger.addSymbolServerUrl": + case "DotnetDebugger.setSymbolOptions": { - string url = args["url"]?.Value(); - if (!string.IsNullOrEmpty(url) && !urlSymbolServerList.Contains(url)) - urlSymbolServerList.Add(url); + SendResponse(id, Result.OkFromObject(new { }), token); + CachePathSymbolServer = args["symbolOptions"]?["cachePath"]?.Value(); + var urls = args["symbolOptions"]?["searchPaths"]?.Value(); + if (urls == null) + return true; + UrlSymbolServerList.Clear(); + UrlSymbolServerList.AddRange(urls.Values()); + if (!JustMyCode) + { + if (!await IsRuntimeAlreadyReadyAlready(id, token)) + return true; + return await ReloadSymbolsFromSymbolServer(id, context, token); + } return true; } case "DotnetDebugger.getMethodLocation": @@ -578,6 +591,14 @@ protected override async Task AcceptCommand(MessageId id, JObject parms, C return method.StartsWith("DotnetDebugger.", StringComparison.OrdinalIgnoreCase); } + private async Task ReloadSymbolsFromSymbolServer(SessionId id, ExecutionContext context, CancellationToken token) + { + DebugStore store = await LoadStore(id, true, token); + store.UpdateSymbolStore(UrlSymbolServerList, CachePathSymbolServer); + await store.ReloadAllPDBsFromSymbolServersAndSendSources(this, id, context, token); + return true; + } + private async Task ApplyUpdates(MessageId id, JObject args, CancellationToken token) { var context = GetContext(id); @@ -590,8 +611,14 @@ private async Task ApplyUpdates(MessageId id, JObject args, CancellationTo return applyUpdates; } - private void SetJustMyCode(MessageId id, bool isEnabled, CancellationToken token) + private async Task SetJustMyCode(MessageId id, bool isEnabled, ExecutionContext context, CancellationToken token) { + if (JustMyCode != isEnabled && isEnabled == false) + { + JustMyCode = isEnabled; + if (await IsRuntimeAlreadyReadyAlready(id, token)) + await ReloadSymbolsFromSymbolServer(id, context, token); + } JustMyCode = isEnabled; SendResponse(id, Result.OkFromObject(new { justMyCodeEnabled = JustMyCode }), token); } @@ -886,7 +913,7 @@ private async Task SendBreakpointsOfMethodUpdated(SessionId sessionId, Exe return true; } - protected virtual async Task ShouldSkipMethod(SessionId sessionId, ExecutionContext context, EventKind event_kind, int j, MethodInfoWithDebugInformation method, CancellationToken token) + protected virtual async Task ShouldSkipMethod(SessionId sessionId, ExecutionContext context, EventKind event_kind, int frameNumber, MethodInfoWithDebugInformation method, CancellationToken token) { var shouldReturn = await SkipMethod( isSkippable: context.IsSkippingHiddenMethod, @@ -904,7 +931,10 @@ protected virtual async Task ShouldSkipMethod(SessionId sessionId, Executi if (shouldReturn) return true; - if (j == 0 && method?.Info.DebuggerAttrInfo.DoAttributesAffectCallStack(JustMyCode) == true) + if (frameNumber != 0) + return false; + + if (method?.Info?.DebuggerAttrInfo?.DoAttributesAffectCallStack(JustMyCode) == true) { if (method.Info.DebuggerAttrInfo.ShouldStepOut(event_kind)) { @@ -929,6 +959,16 @@ protected virtual async Task ShouldSkipMethod(SessionId sessionId, Executi context.IsResumedAfterBp = true; } } + else + { + if (!JustMyCode && method?.Info?.DebuggerAttrInfo?.HasNonUserCode == true && !method.Info.hasDebugInformation) + { + if (event_kind == EventKind.Step) + context.IsSkippingHiddenMethod = true; + if (await SkipMethod(isSkippable: true, shouldBeSkipped: true, StepKind.Out)) + return true; + } + } return false; async Task SkipMethod(bool isSkippable, bool shouldBeSkipped, StepKind stepKind) { @@ -1132,49 +1172,6 @@ internal async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObje return false; } - internal async Task LoadSymbolsOnDemand(AssemblyInfo asm, int method_token, SessionId sessionId, CancellationToken token) - { - ExecutionContext context = GetContext(sessionId); - if (urlSymbolServerList.Count == 0) - return null; - if (asm.TriedToLoadSymbolsOnDemand || !asm.CodeViewInformationAvailable) - return null; - asm.TriedToLoadSymbolsOnDemand = true; - var pdbName = Path.GetFileName(asm.PdbName); - - foreach (string urlSymbolServer in urlSymbolServerList) - { - string downloadURL = $"{urlSymbolServer}/{pdbName}/{asm.PdbGuid.ToString("N").ToUpperInvariant() + asm.PdbAge}/{pdbName}"; - - try - { - using HttpResponseMessage response = await HttpClient.GetAsync(downloadURL, token); - if (!response.IsSuccessStatusCode) - { - Log("info", $"Unable to download symbols on demand url:{downloadURL} assembly: {asm.Name}"); - continue; - } - - using Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(token); - asm.UpdatePdbInformation(streamToReadFrom); - foreach (SourceFile source in asm.Sources) - { - var scriptSource = JObject.FromObject(source.ToScriptSource(context.Id, context.AuxData)); - await SendEvent(sessionId, "Debugger.scriptParsed", scriptSource, token); - } - return asm.GetMethodByToken(method_token); - } - catch (Exception e) - { - Log("info", $"Unable to load symbols on demand exception: {e} url:{downloadURL} assembly: {asm.Name}"); - } - break; - } - - Log("info", $"Unable to load symbols on demand assembly: {asm.Name}"); - return null; - } - protected void OnDefaultContextUpdate(SessionId sessionId, ExecutionContext context) { if (UpdateContext(sessionId, context, out ExecutionContext previousContext)) @@ -1575,7 +1572,7 @@ private static IEnumerable> GetBPReqLocation { var comparer = new SourceLocation.LocationComparer(); // if column is specified the frontend wants the exact matches - // and will clear the bp if it isn't close enoug + // and will clear the bp if it isn't close enough var bpLocations = store.FindBreakpointLocations(req, ifNoneFoundThenFindNext); IEnumerable> locations = bpLocations.Distinct(comparer) .OrderBy(l => l.Column) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index 29d6356acb09e..8f0556b12f634 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -890,19 +890,6 @@ public async Task GetMethodInfo(int methodId, Ca var method = asm.GetMethodByToken(methodToken); - if (method == null && !asm.HasSymbols) - { - try - { - method = await proxy.LoadSymbolsOnDemand(asm, methodToken, sessionId, token); - } - catch (Exception e) - { - logger.LogDebug($"Unable to find method token: {methodToken} assembly name: {asm.Name} exception: {e}"); - return null; - } - } - string methodName = await GetMethodName(methodId, token); //get information from runtime method ??= await CreateMethodInfoFromRuntimeInformation(asm, methodId, methodName, methodToken, token); diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/ChromeProvider.cs b/src/mono/wasm/debugger/DebuggerTestSuite/ChromeProvider.cs index d13dc96434882..fc7878af3bf68 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/ChromeProvider.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/ChromeProvider.cs @@ -83,7 +83,7 @@ public async Task StartBrowserAndProxyAsync(HttpContext context, _logger.LogInformation($"{messagePrefix} launching proxy for {con_str}"); - _debuggerProxy = new DebuggerProxy(loggerFactory, null, loggerId: Id); + _debuggerProxy = new DebuggerProxy(loggerFactory, loggerId: Id); TestHarnessProxy.RegisterNewProxy(Id, _debuggerProxy); var browserUri = new Uri(con_str); WebSocket? ideSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs index d5cac7e9a3d4f..673217c64fa9b 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs @@ -54,6 +54,7 @@ public static WasmHost RunningOn private const int DefaultTestTimeoutMs = 1 * 60 * 1000; protected TimeSpan TestTimeout = TimeSpan.FromMilliseconds(DefaultTestTimeoutMs); protected ITestOutputHelper _testOutput; + protected readonly TestEnvironment _env; static string s_debuggerTestAppPath; static int s_idCounter = -1; @@ -117,8 +118,16 @@ public static string TestLogPath } } + public static string TempPath => Path.Combine(Path.GetTempPath(), "dbg-tests-tmp"); + static DebuggerTestBase() + { + if (Directory.Exists(TempPath)) + Directory.Delete(TempPath, recursive: true); + } + public DebuggerTestBase(ITestOutputHelper testOutput, string driver = "debugger-driver.html") { + _env = new TestEnvironment(testOutput); _testOutput = testOutput; Id = Interlocked.Increment(ref s_idCounter); // the debugger is working in locale of the debugged application. For example Datetime.ToString() @@ -151,7 +160,11 @@ public virtual async Task InitializeAsync() await insp.OpenSessionAsync(fn, TestTimeout); } - public virtual async Task DisposeAsync() => await insp.ShutdownAsync().ConfigureAwait(false); + public virtual async Task DisposeAsync() + { + await insp.ShutdownAsync().ConfigureAwait(false); + _env.Dispose(); + } public Task Ready() => startTask; @@ -1508,6 +1521,13 @@ internal async Task SetJustMyCode(bool enabled) Assert.Equal(res.Value["justMyCodeEnabled"], enabled); } + + internal async Task SetSymbolOptions(JObject param) + { + var res = await cli.SendCommand("DotnetDebugger.setSymbolOptions", param, token); + Assert.True(res.IsOk); + } + internal async Task CheckEvaluateFail(string id, params (string expression, string message)[] args) { foreach (var arg in args) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EnvironmentVariables.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EnvironmentVariables.cs index 1e52ee8397573..0091ef0a02781 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EnvironmentVariables.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EnvironmentVariables.cs @@ -11,4 +11,6 @@ internal static class EnvironmentVariables { public static readonly string? DebuggerTestPath = Environment.GetEnvironmentVariable("DEBUGGER_TEST_PATH"); public static readonly string? TestLogPath = Environment.GetEnvironmentVariable("TEST_LOG_PATH"); + public static readonly bool SkipCleanup = Environment.GetEnvironmentVariable("SKIP_CLEANUP") == "1" || + Environment.GetEnvironmentVariable("SKIP_CLEANUP") == "true"; } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/SteppingTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/SteppingTests.cs index a3d4c0bf2ce72..868eb1d7006bf 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/SteppingTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/SteppingTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.IO; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; @@ -877,7 +878,7 @@ await EvaluateAndCheck( "dotnet://debugger-test.dll/debugger-async-test.cs", line_pause, column_pause, $"DebuggerTests.AsyncTests.ContinueWithTests.{method_name}"); } - + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(112, 16, 114, 16, "HiddenLinesInAnAsyncBlock")] [InlineData(130, 16, 133, 16, "HiddenLinesJustBeforeANestedAsyncBlock")] @@ -1037,5 +1038,182 @@ await EvaluateAndCheck( step_into2["callFrames"][0]["location"]["lineNumber"].Value() ); } + + [ConditionalTheory(nameof(RunningOnChrome))] + [InlineData(true)] + [InlineData(false)] + public async Task SteppingIntoLibrarySymbolsLoadedFromSymbolServer(bool justMyCode) + { + string cachePath = _env.CreateTempDirectory("symbols-cache"); + _testOutput.WriteLine($"** Using cache path: {cachePath}"); + var searchPaths = new JArray + { + "https://symbols.nuget.org/download/symbols" + }; + var waitForScript = WaitForScriptParsedEventsAsync(new string [] { "JArray.cs" }); + var symbolOptions = JObject.FromObject(new { symbolOptions = JObject.FromObject(new { cachePath, searchPaths })}); + await SetJustMyCode(justMyCode); + await SetSymbolOptions(symbolOptions); + + await EvaluateAndCheck( + "window.setTimeout(function() { invoke_static_method ('[debugger-test] TestLoadSymbols:Run'); }, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", 1572, 8, + "TestLoadSymbols.Run" + ); + if (!justMyCode) + await waitForScript; + + await StepAndCheck(StepKind.Into, justMyCode ? "dotnet://debugger-test.dll/debugger-test.cs" : "dotnet://Newtonsoft.Json.dll/JArray.cs", justMyCode ? 1575 : 350, justMyCode ? 8 : 12, justMyCode ? "TestLoadSymbols.Run" : "Newtonsoft.Json.Linq.JArray.Add", + locals_fn: async (locals) => + { + if (!justMyCode) + await CheckObject(locals, "this", "Newtonsoft.Json.Linq.JArray", description: "[]"); + else + await CheckObject(locals, "array", "Newtonsoft.Json.Linq.JArray", description: "[\n \"Manual text\"\n]"); + }, times: 2 + ); + } + + [ConditionalFact(nameof(RunningOnChrome))] + public async Task SteppingIntoLibraryWithoutSymbolsAndStepAgainAfterLoadSymbols() + { + string cachePath = _env.CreateTempDirectory("symbols-cache"); + _testOutput.WriteLine($"** Using cache path: {cachePath}"); + var searchPaths = new JArray + { + "https://symbols.nuget.org/download/symbols" + }; + var waitForScript = WaitForScriptParsedEventsAsync(new string [] { "JArray.cs" }); + var symbolOptions = JObject.FromObject(new { symbolOptions = JObject.FromObject(new { cachePath, searchPaths })}); + await SetJustMyCode(false); + + await EvaluateAndCheck( + "window.setTimeout(function() { invoke_static_method ('[debugger-test] TestLoadSymbols:Run'); invoke_static_method ('[debugger-test] TestLoadSymbols:Run'); }, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", 1572, 8, + "TestLoadSymbols.Run" + ); + await StepAndCheck(StepKind.Into, "dotnet://debugger-test.dll/debugger-test.cs", 1575, 8, "TestLoadSymbols.Run", + locals_fn: async (locals) => + { + await CheckObject(locals, "array", "Newtonsoft.Json.Linq.JArray", description: "[\n \"Manual text\"\n]"); + }, times: 2 + ); + + await SetSymbolOptions(symbolOptions); + await waitForScript; + + await SendCommandAndCheck(null, "Debugger.resume", + "dotnet://debugger-test.dll/debugger-test.cs", 1572, 8, + "TestLoadSymbols.Run" + ); + + await StepAndCheck(StepKind.Into, "dotnet://Newtonsoft.Json.dll/JArray.cs", 350, 12, "Newtonsoft.Json.Linq.JArray.Add", + locals_fn: async (locals) => + { + await CheckObject(locals, "this", "Newtonsoft.Json.Linq.JArray", description: "[]"); + }, times: 2 + ); + } + + [ConditionalFact(nameof(RunningOnChrome))] + public async Task SteppingIntoLibrarySymbolsLoadedFromSymbolServerAddOtherSymbolServerAndStepAgain() + { + string cachePath = _env.CreateTempDirectory("symbols-cache"); + _testOutput.WriteLine($"** Using cache path: {cachePath}"); + + var searchPaths = new JArray + { + "https://symbols.nuget.org/download/symbols" + }; + var waitForScript = WaitForScriptParsedEventsAsync(new string [] { "JArray.cs" }); + var symbolOptions = JObject.FromObject(new { symbolOptions = JObject.FromObject(new { cachePath, searchPaths })}); + await SetJustMyCode(false); + await SetSymbolOptions(symbolOptions); + + await EvaluateAndCheck( + "window.setTimeout(function() { invoke_static_method ('[debugger-test] TestLoadSymbols:Run'); invoke_static_method ('[debugger-test] TestLoadSymbols:Run'); }, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", 1572, 8, + "TestLoadSymbols.Run" + ); + + await waitForScript; + + await StepAndCheck(StepKind.Into, "dotnet://Newtonsoft.Json.dll/JArray.cs", 350, 12, "Newtonsoft.Json.Linq.JArray.Add", + locals_fn: async (locals) => + { + await CheckObject(locals, "this", "Newtonsoft.Json.Linq.JArray", description: "[]"); + }, times: 2 + ); + + searchPaths.Add("https://msdl.microsoft.com/download/symbols"); + symbolOptions = JObject.FromObject(new { symbolOptions = JObject.FromObject(new { cachePath, searchPaths })}); + await SetSymbolOptions(symbolOptions); + + await SendCommandAndCheck(null, "Debugger.resume", + "dotnet://debugger-test.dll/debugger-test.cs", 1572, 8, + "TestLoadSymbols.Run" + ); + + await StepAndCheck(StepKind.Into, "dotnet://Newtonsoft.Json.dll/JArray.cs", 350, 12, "Newtonsoft.Json.Linq.JArray.Add", + locals_fn: async (locals) => + { + await CheckObject(locals, "this", "Newtonsoft.Json.Linq.JArray", description: "[]"); + }, times: 2 + ); + } + + [ConditionalTheory(nameof(RunningOnChrome))] + [InlineData("https://symbols.nuget.org/download/symbols", "")] + // Symbols are already loaded, so setting urls = [] won't affect it + [InlineData] + [InlineData("", "https://microsoft.com/non-existant/symbols")] + public async Task SteppingIntoLibrarySymbolsLoadedFromSymbolServerRemoveSymbolServerAndStepAgain(params string[] secondServers) + { + string cachePath = _env.CreateTempDirectory("symbols-cache"); + _testOutput.WriteLine($"Using cachePath: {cachePath}"); + var searchPaths = new JArray + { + "https://symbols.nuget.org/download/symbols", + "https://msdl.microsoft.com/download/bad-non-existant", + "https://msdl.microsoft.com/download/symbols" + }; + var waitForScript = WaitForScriptParsedEventsAsync(new string [] { "JArray.cs" }); + var symbolOptions = JObject.FromObject(new { symbolOptions = JObject.FromObject(new { cachePath, searchPaths })}); + await SetJustMyCode(false); + await SetSymbolOptions(symbolOptions); + + await EvaluateAndCheck( + "window.setTimeout(function() { invoke_static_method ('[debugger-test] TestLoadSymbols:Run'); invoke_static_method ('[debugger-test] TestLoadSymbols:Run'); }, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", 1572, 8, + "TestLoadSymbols.Run" + ); + + await waitForScript; + + await StepAndCheck(StepKind.Into, "dotnet://Newtonsoft.Json.dll/JArray.cs", 350, 12, "Newtonsoft.Json.Linq.JArray.Add", + locals_fn: async (locals) => + { + await CheckObject(locals, "this", "Newtonsoft.Json.Linq.JArray", description: "[]"); + }, times: 2 + ); + searchPaths.Clear(); + foreach (string secondServer in secondServers) + searchPaths.Add(secondServer); + + symbolOptions = JObject.FromObject(new { symbolOptions = JObject.FromObject(new { cachePath, searchPaths })}); + await SetSymbolOptions(symbolOptions); + + await SendCommandAndCheck(null, "Debugger.resume", + "dotnet://debugger-test.dll/debugger-test.cs", 1572, 8, + "TestLoadSymbols.Run" + ); + + await StepAndCheck(StepKind.Into, "dotnet://Newtonsoft.Json.dll/JArray.cs", 350, 12, "Newtonsoft.Json.Linq.JArray.Add", + locals_fn: async (locals) => + { + await CheckObject(locals, "this", "Newtonsoft.Json.Linq.JArray", description: "[]"); + }, times: 2 + ); + } } } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/TestEnvironment.cs b/src/mono/wasm/debugger/DebuggerTestSuite/TestEnvironment.cs new file mode 100644 index 0000000000000..71f1a29b09591 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/TestEnvironment.cs @@ -0,0 +1,42 @@ +// 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.IO; +using System.Linq; +using Xunit.Abstractions; + +namespace DebuggerTests; + +public class TestEnvironment : IDisposable +{ + private bool _disposed; + private readonly string _tempPath; + private readonly ITestOutputHelper _testOutput; + + public TestEnvironment(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + _tempPath = Path.Combine(DebuggerTestBase.TempPath, Guid.NewGuid().ToString()); + if (Directory.Exists(_tempPath)) + Directory.Delete(_tempPath, recursive: true); + + Directory.CreateDirectory(_tempPath); + } + + public void Dispose() + { + if (_disposed || EnvironmentVariables.SkipCleanup) + return; + + Directory.Delete(_tempPath, recursive: true); + _disposed = true; + } + + public string CreateTempDirectory(string relativeDir, params string[] relativePathParts) + { + string newPath = Path.Combine(_tempPath, relativeDir, Path.Combine(relativePathParts)); + Directory.CreateDirectory(newPath); + return newPath; + } +} 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 d024edf51d7eb..b7d397d6d25c2 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs @@ -1561,4 +1561,18 @@ public static void Run() var n = new ToStringOverridenN(); System.Diagnostics.Debugger.Break(); } -} \ No newline at end of file +} +public class TestLoadSymbols +{ + public static void Run() + { + var array = new Newtonsoft.Json.Linq.JArray(); + var text = new Newtonsoft.Json.Linq.JValue("Manual text"); + var date = new Newtonsoft.Json.Linq.JValue(new DateTime(2000, 5, 23)); + + System.Diagnostics.Debugger.Break(); + + array.Add(text); + array.Add(date); + } +} 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 7f6732875e3c5..4d01709f69382 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj @@ -8,7 +8,12 @@ true library true + true + + + + @@ -66,6 +71,7 @@ +