[wasm][debugger] Implement support to symbolOptions from dap. (#79284)
authorThays Grazia <thaystg@gmail.com>
Mon, 23 Jan 2023 22:43:32 +0000 (19:43 -0300)
committerGitHub <noreply@github.com>
Mon, 23 Jan 2023 22:43:32 +0000 (19:43 -0300)
* 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 <lewing@microsoft.com>
Co-authored-by: Ankit Jain <radical@gmail.com>
* adressing @radical comments.

* Addressing @radical comment.

* Addressing @radical comments.

* Apply suggestions from code review

Co-authored-by: Ankit Jain <radical@gmail.com>
* 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 <lewing@microsoft.com>
Co-authored-by: Ankit Jain <radical@gmail.com>
18 files changed:
eng/Versions.props
src/mono/mono/component/debugger-engine.c
src/mono/nuget/Microsoft.NETCore.BrowserDebugHost.Transport/Microsoft.NETCore.BrowserDebugHost.Transport.pkgproj
src/mono/wasm/debugger/BrowserDebugHost/Startup.cs
src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.csproj
src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs
src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs
src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs
src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs
src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
src/mono/wasm/debugger/DebuggerTestSuite/ChromeProvider.cs
src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
src/mono/wasm/debugger/DebuggerTestSuite/EnvironmentVariables.cs
src/mono/wasm/debugger/DebuggerTestSuite/SteppingTests.cs
src/mono/wasm/debugger/DebuggerTestSuite/TestEnvironment.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs
src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj

index 3a5c1a5..a57587c 100644 (file)
     <runtimewinx64MicrosoftNETCoreRuntimeJITToolsVersion>1.0.0-alpha.1.23066.1</runtimewinx64MicrosoftNETCoreRuntimeJITToolsVersion>
     <runtimeosx110arm64MicrosoftNETCoreRuntimeJITToolsVersion>1.0.0-alpha.1.23066.1</runtimeosx110arm64MicrosoftNETCoreRuntimeJITToolsVersion>
     <runtimeosx1012x64MicrosoftNETCoreRuntimeJITToolsVersion>1.0.0-alpha.1.23066.1</runtimeosx1012x64MicrosoftNETCoreRuntimeJITToolsVersion>
+
+    <!-- BrowserDebugProxy libs -->
+    <MicrosoftExtensionsLoggingVersion>3.1.7</MicrosoftExtensionsLoggingVersion>
+    <MicrosoftSymbolStoreVersion>1.0.406601</MicrosoftSymbolStoreVersion>
   </PropertyGroup>
 </Project>
index 50a3a0f..2a05aba 100644 (file)
@@ -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);
index 2119425..8294b2c 100644 (file)
@@ -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" />
 
     <PackageFile Include="@(_browserDebugHostFiles)" TargetPath="tools\$(NetCoreAppCurrent)\" />
   </ItemGroup>
index 9e092af..a685686 100644 (file)
@@ -203,8 +203,7 @@ namespace Microsoft.WebAssembly.Diagnostics
                     try
                     {
                         var loggerFactory = context.RequestServices.GetService<ILoggerFactory>();
-                        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();
 
index eb93b1a..22f0a96 100644 (file)
@@ -8,10 +8,11 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.7.0" />
-    <PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.7" />
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="$(MicrosoftCodeAnalysisCSharpVersion)" />
+    <PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingVersion)" />
+    <PackageReference Include="Microsoft.SymbolStore" Version="$(MicrosoftSymbolStoreVersion)" />
     <PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
-    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.7.0" />
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisCSharpVersion)" />
   </ItemGroup>
 
   <ItemGroup>
index d053921..06ddcb0 100644 (file)
@@ -21,6 +21,10 @@ using System.IO.Compression;
 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 @@ namespace Microsoft.WebAssembly.Diagnostics
     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 @@ namespace Microsoft.WebAssembly.Diagnostics
         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 @@ namespace Microsoft.WebAssembly.Diagnostics
         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 @@ namespace Microsoft.WebAssembly.Diagnostics
             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<int>() != 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<AsyncScopeDebugInformation>();
             }
+            if (!hasDebugInformation)
+                DebuggerAttrInfo.HasNonUserCode = true;
+            DebuggerAttrInfo.ClearInsignificantAttrFlags();
         }
 
         public bool ContainsAsyncScope(int oneBasedIdx, int offset)
@@ -512,24 +460,41 @@ namespace Microsoft.WebAssembly.Diagnostics
             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<int>() != 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 @@ namespace Microsoft.WebAssembly.Diagnostics
                 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<AsyncScopeDebugInformation>();
+        }
+
+        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 @@ namespace Microsoft.WebAssembly.Diagnostics
         private readonly IDisposable peReaderOrWebcilReader;
         internal MetadataReader asmMetadataReader { get; }
         internal MetadataReader pdbMetadataReader { get; set; }
+
         internal List<Tuple<MetadataReader, MetadataReader>> enCMetadataReader  = new List<Tuple<MetadataReader, MetadataReader>>();
         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<int, SourceFile> _documentIdToSourceFileTable = new Dictionary<int, SourceFile>();
+        public PdbChecksum[] PdbChecksums { get; }
 
         public static AssemblyInfo FromBytes(MonoProxy monoProxy, SessionId sessionId, byte[] assembly, byte[] pdb, ILogger logger, CancellationToken token)
         {
@@ -904,12 +899,20 @@ namespace Microsoft.WebAssembly.Diagnostics
         {
             var entries = peReader.ReadDebugDirectory();
             CodeViewDebugDirectoryData? codeViewData = null;
-            if (entries.Length > 0)
+            var isPortableCodeView = false;
+            List<PdbChecksum> 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 @@ namespace Microsoft.WebAssembly.Diagnostics
                 }
             }
 
-            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 @@ namespace Microsoft.WebAssembly.Diagnostics
         {
             var entries = wcReader.ReadDebugDirectory();
             CodeViewDebugDirectoryData? codeViewData = null;
-            if (entries.Length > 0)
+            var isPortableCodeView = false;
+            List<PdbChecksum> 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 @@ namespace Microsoft.WebAssembly.Diagnostics
                 }
             }
 
-            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 @@ namespace Microsoft.WebAssembly.Diagnostics
             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 @@ namespace Microsoft.WebAssembly.Diagnostics
                 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 @@ namespace Microsoft.WebAssembly.Diagnostics
                 }
             }
         }
-        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 @@ namespace Microsoft.WebAssembly.Diagnostics
             }
         }
 
-        private Uri GetSourceLinkUrl(string document)
-        {
-            if (sourceLinkMappings.TryGetValue(document, out string url))
-                return new Uri(url);
-
-            foreach (KeyValuePair<string, string> 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 @@ namespace Microsoft.WebAssembly.Diagnostics
             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 @@ namespace Microsoft.WebAssembly.Diagnostics
         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<MethodInfo> 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<string, string> sourceLinkMappings)
         {
             this.methods = new Dictionary<int, MethodInfo>();
-            this.SourceLinkUri = sourceLinkUri;
+            GetSourceLinkUrl(documentName, sourceLinkMappings);
             this.assembly = assembly;
             this.Id = id;
             this.doc = assembly.pdbMetadataReader.GetDocument(docHandle);
@@ -1321,7 +1333,66 @@ namespace Microsoft.WebAssembly.Diagnostics
             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<string, string> sourceLinkMappings)
+        {
+            if (sourceLinkMappings.TryGetValue(document, out string url))
+            {
+                SourceLinkUri = new Uri(url);
+                return;
+            }
+
+            foreach (KeyValuePair<string, string> 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 @@ namespace Microsoft.WebAssembly.Diagnostics
     {
         internal List<AssemblyInfo> assemblies = new List<AssemblyInfo>();
         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 @@ namespace Microsoft.WebAssembly.Diagnostics
         }
 
         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<string> 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);
+        }
     }
 }
index c4e7c5e..5bac4e3 100644 (file)
@@ -18,10 +18,10 @@ namespace Microsoft.WebAssembly.Diagnostics
     {
         internal MonoProxy MonoProxy { get; }
 
-        public DebuggerProxy(ILoggerFactory loggerFactory, IList<string> 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)
index 43565a2..829dc3a 100644 (file)
@@ -58,7 +58,7 @@ namespace Microsoft.WebAssembly.Diagnostics
                 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 @@ namespace Microsoft.WebAssembly.Diagnostics
             // 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 @@ namespace Microsoft.WebAssembly.Diagnostics
             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];
             }
index 3d811d8..267e9bc 100644 (file)
@@ -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)
     {
     }
 
index d0f68b0..3733c6a 100644 (file)
@@ -18,7 +18,8 @@ namespace Microsoft.WebAssembly.Diagnostics
 {
     internal class MonoProxy : DevToolsProxy
     {
-        private IList<string> urlSymbolServerList;
+        internal List<string> UrlSymbolServerList { get; private set; }
+        internal string CachePathSymbolServer { get; private set; }
         private HashSet<SessionId> sessions = new HashSet<SessionId>();
         private static readonly string[] s_executionContextIndependentCDPCommandNames = { "DotnetDebugger.setDebuggerProperty", "DotnetDebugger.runTests" };
         protected Dictionary<SessionId, ExecutionContext> contexts = new Dictionary<SessionId, ExecutionContext>();
@@ -32,9 +33,9 @@ namespace Microsoft.WebAssembly.Diagnostics
 
         protected readonly ProxyOptions _options;
 
-        public MonoProxy(ILogger logger, IList<string> 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<string>();
+            UrlSymbolServerList = new List<string>();
             RuntimeId = runtimeId;
             _options = options;
             _defaultPauseOnExceptions = PauseOnExceptionsKind.Unset;
@@ -76,10 +77,10 @@ namespace Microsoft.WebAssembly.Diagnostics
             {
                 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 @@ namespace Microsoft.WebAssembly.Diagnostics
                             bool? is_default = aux_data["isDefault"]?.Value<bool>();
                             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 @@ namespace Microsoft.WebAssembly.Diagnostics
                                 {
                                     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 @@ namespace Microsoft.WebAssembly.Diagnostics
 
             if (!contexts.TryGetValue(id, out ExecutionContext context) && !s_executionContextIndependentCDPCommandNames.Contains(method))
             {
-                if  (method == "Debugger.setPauseOnExceptions")
+                if (method == "Debugger.setPauseOnExceptions")
                 {
                     string state = args["state"].Value<string>();
                     var pauseOnException = GetPauseOnExceptionsStatusFromString(state);
@@ -505,11 +508,11 @@ namespace Microsoft.WebAssembly.Diagnostics
                             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 @@ namespace Microsoft.WebAssembly.Diagnostics
                             SendResponse(id, Result.Err("ApplyUpdate failed."), token);
                         return true;
                     }
-                case "DotnetDebugger.addSymbolServerUrl":
+                case "DotnetDebugger.setSymbolOptions":
                     {
-                        string url = args["url"]?.Value<string>();
-                        if (!string.IsNullOrEmpty(url) && !urlSymbolServerList.Contains(url))
-                            urlSymbolServerList.Add(url);
+                        SendResponse(id, Result.OkFromObject(new { }), token);
+                        CachePathSymbolServer = args["symbolOptions"]?["cachePath"]?.Value<string>();
+                        var urls = args["symbolOptions"]?["searchPaths"]?.Value<JArray>();
+                        if (urls == null)
+                            return true;
+                        UrlSymbolServerList.Clear();
+                        UrlSymbolServerList.AddRange(urls.Values<string>());
+                        if (!JustMyCode)
+                        {
+                            if (!await IsRuntimeAlreadyReadyAlready(id, token))
+                                return true;
+                            return await ReloadSymbolsFromSymbolServer(id, context, token);
+                        }
                         return true;
                     }
                 case "DotnetDebugger.getMethodLocation":
@@ -578,6 +591,14 @@ namespace Microsoft.WebAssembly.Diagnostics
             return method.StartsWith("DotnetDebugger.", StringComparison.OrdinalIgnoreCase);
         }
 
+        private async Task<bool> 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<bool> ApplyUpdates(MessageId id, JObject args, CancellationToken token)
         {
             var context = GetContext(id);
@@ -590,8 +611,14 @@ namespace Microsoft.WebAssembly.Diagnostics
             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 @@ namespace Microsoft.WebAssembly.Diagnostics
             return true;
         }
 
-        protected virtual async Task<bool> ShouldSkipMethod(SessionId sessionId, ExecutionContext context, EventKind event_kind, int j, MethodInfoWithDebugInformation method, CancellationToken token)
+        protected virtual async Task<bool> 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 @@ namespace Microsoft.WebAssembly.Diagnostics
             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 @@ namespace Microsoft.WebAssembly.Diagnostics
                         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<bool> SkipMethod(bool isSkippable, bool shouldBeSkipped, StepKind stepKind)
             {
@@ -1132,49 +1172,6 @@ namespace Microsoft.WebAssembly.Diagnostics
             return false;
         }
 
-        internal async Task<MethodInfo> 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 @@ namespace Microsoft.WebAssembly.Diagnostics
         {
             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<IGrouping<SourceId, SourceLocation>> locations = bpLocations.Distinct(comparer)
                 .OrderBy(l => l.Column)
index 29d6356..8f0556b 100644 (file)
@@ -890,19 +890,6 @@ namespace Microsoft.WebAssembly.Diagnostics
 
             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);
index d13dc96..fc7878a 100644 (file)
@@ -83,7 +83,7 @@ internal class ChromeProvider : WasmHostProvider
 
         _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);
index d5cac7e..673217c 100644 (file)
@@ -54,6 +54,7 @@ namespace DebuggerTests
         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 @@ namespace DebuggerTests
             }
         }
 
+        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 @@ namespace DebuggerTests
             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 @@ namespace DebuggerTests
             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)
index 1e52ee8..0091ef0 100644 (file)
@@ -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";
 }
index a3d4c0b..868eb1d 100644 (file)
@@ -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 @@ namespace DebuggerTests
                 "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 @@ namespace DebuggerTests
                 step_into2["callFrames"][0]["location"]["lineNumber"].Value<int>()
                 );
         }
+
+        [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 (file)
index 0000000..71f1a29
--- /dev/null
@@ -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;
+    }
+}
index d024edf..b7d397d 100644 (file)
@@ -1561,4 +1561,18 @@ public class ToStringOverriden
         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);
+    }
+}
index 7f67328..4d01709 100644 (file)
@@ -8,7 +8,12 @@
     <WasmGenerateAppBundle>true</WasmGenerateAppBundle>
     <OutputType>library</OutputType>
     <WasmEmitSymbolMap>true</WasmEmitSymbolMap>
+    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
   </PropertyGroup>
+  <ItemGroup>
+    <!-- keep this version to make sure it will pause in the expected line -->
+    <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
+  </ItemGroup>
 
   <ItemGroup>
     <WasmExtraFilesToDeploy Include="debugger-driver.html" />
@@ -66,6 +71,7 @@
       <WasmAssembliesToBundle Include="$(OutDir)\debugger-test-with-non-user-code-class.dll" />
       <WasmAssembliesToBundle Condition="!$([MSBuild]::IsOSPlatform('windows'))" Include="$(OutDir)\debugger-test-with-colon-in-source-name.dll" />
       <WasmAssembliesToBundle Include="$(OutDir)\debugger-test-vb.dll" />
+      <WasmAssembliesToBundle Include="$(OutDir)\Newtonsoft.Json.dll" />
       <WasmAssembliesToBundle Include="$(MicrosoftNetCoreAppRuntimePackRidDir)\lib\$(NetCoreappCurrent)\System.Runtime.InteropServices.JavaScript.dll" />
 
       <!-- Assemblies only dynamically loaded -->