From 6c5e1690ad6e4aa7a2fc1dfbbf193f8f6511fbc6 Mon Sep 17 00:00:00 2001 From: Steve Sanderson <SteveSandersonMS@users.noreply.github.com> Date: Thu, 23 May 2019 13:10:53 +0100 Subject: [PATCH] Update Mono WebAssembly for Blazor (https://github.com/aspnet/Blazor/pull/1807) --- build/sources.props | 5 + eng/Versions.props | 2 +- .../src/targets/Blazor.MonoRuntime.props | 2 +- .../src/MonoDebugProxy/UpdateSources.cmd | 20 -- .../src/MonoDebugProxy/ws-proxy/DebugStore.cs | 261 +++++++++++++----- .../src/MonoDebugProxy/ws-proxy/MonoProxy.cs | 157 +++++++++-- .../src/MonoDebugProxy/ws-proxy/WsProxy.cs | 6 +- 7 files changed, 330 insertions(+), 123 deletions(-) delete mode 100644 src/Components/Blazor/Server/src/MonoDebugProxy/UpdateSources.cmd diff --git a/build/sources.props b/build/sources.props index bcfbefc9962..26d8be66778 100644 --- a/build/sources.props +++ b/build/sources.props @@ -27,6 +27,11 @@ $(RestoreSources); https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; </RestoreSources> + <!-- TODO remove this once we move Microsoft.AspNetCore.Blazor.Mono to a non-myget feed --> + <RestoreSources> + $(RestoreSources); + https://dotnet.myget.org/F/blazor-dev/api/v3/index.json; + </RestoreSources> <!-- In an orchestrated build, this may be overriden to other Azure feeds. --> <DotNetAssetRootUrl Condition="'$(DotNetAssetRootUrl)'==''">https://dotnetcli.blob.core.windows.net/dotnet/</DotNetAssetRootUrl> diff --git a/eng/Versions.props b/eng/Versions.props index bc32721fd6d..10a317d4415 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -165,7 +165,7 @@ <MicrosoftWebXdtPackageVersion>1.4.0</MicrosoftWebXdtPackageVersion> <SystemIdentityModelTokensJwtPackageVersion>5.3.0</SystemIdentityModelTokensJwtPackageVersion> <!-- Dependencies for Blazor. --> - <MicrosoftAspNetCoreBlazorMonoPackageVersion>0.10.0-preview-20190325.1</MicrosoftAspNetCoreBlazorMonoPackageVersion> + <MicrosoftAspNetCoreBlazorMonoPackageVersion>0.10.0-preview-20190523.1</MicrosoftAspNetCoreBlazorMonoPackageVersion> <!-- Packages from 2.1/2.2 branches used for site extension build --> <MicrosoftAspNetCoreAzureAppServicesSiteExtension21PackageVersion>2.1.1</MicrosoftAspNetCoreAzureAppServicesSiteExtension21PackageVersion> <MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion>2.2.0</MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion> diff --git a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props index 682dbeed28d..03f70748ffb 100644 --- a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props +++ b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props @@ -6,7 +6,7 @@ <PropertyGroup Label="Blazor build outputs"> <MonoLinkerI18NAssemblies>none</MonoLinkerI18NAssemblies> <!-- See Mono linker docs - allows comma-separated values from: none,all,cjk,mideast,other,rare,west --> - <AdditionalMonoLinkerOptions>--verbose --strip-security true --exclude-feature com --exclude-feature sre -v false -c link -u link -b true</AdditionalMonoLinkerOptions> + <AdditionalMonoLinkerOptions>--disable-opt unreachablebodies --verbose --strip-security true --exclude-feature com --exclude-feature sre -v false -c link -u link -b true</AdditionalMonoLinkerOptions> <BaseBlazorDistPath>dist/</BaseBlazorDistPath> <BaseBlazorPackageContentOutputPath>$(BaseBlazorDistPath)_content/</BaseBlazorPackageContentOutputPath> <BaseBlazorRuntimeOutputPath>$(BaseBlazorDistPath)_framework/</BaseBlazorRuntimeOutputPath> diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/UpdateSources.cmd b/src/Components/Blazor/Server/src/MonoDebugProxy/UpdateSources.cmd deleted file mode 100644 index 591f2f99504..00000000000 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/UpdateSources.cmd +++ /dev/null @@ -1,20 +0,0 @@ -@echo off -echo |---- -echo | Copying the ws-proxy sources here is a temporary step until ws-proxy is -echo | distributed as a NuGet package. -echo | ... -echo | Instead of dealing with Git submodules, this script simply fetches the -echo | latest sources so they can be built directly inside this project (hence -echo | we don't have to publish our own separate package for this). -echo | ... -echo | When updating, you'll need to re-apply any patches we've made manually. -echo |---- -@echo on - -cd /D "%~dp0" -rmdir /s /q ws-proxy -git clone https://github.com/kumpera/ws-proxy.git -rmdir /s /q ws-proxy\.git -del ws-proxy\*.csproj -del ws-proxy\*.sln -del ws-proxy\Program.cs diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs index 9d29cc494a7..ee28bd94b76 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs @@ -6,6 +6,9 @@ using Mono.Cecil.Cil; using System.Linq; using Newtonsoft.Json.Linq; using System.Net.Http; +using Mono.Cecil.Pdb; +using Newtonsoft.Json; +using System.Text.RegularExpressions; namespace WsProxy { internal class BreakPointRequest { @@ -18,17 +21,29 @@ namespace WsProxy { return $"BreakPointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}"; } - public static BreakPointRequest Parse (JObject args) + public static BreakPointRequest Parse (JObject args, DebugStore store) { if (args == null) return null; var url = args? ["url"]?.Value<string> (); - if (!url.StartsWith ("dotnet://", StringComparison.InvariantCulture)) + if (url == null) { + var urlRegex = args?["urlRegex"].Value<string>(); + var sourceFile = store.GetFileByUrlRegex (urlRegex); + + url = sourceFile?.DotNetUrl; + } + + if (url != null && !url.StartsWith ("dotnet://", StringComparison.InvariantCulture)) { + var sourceFile = store.GetFileByUrl (url); + url = sourceFile?.DotNetUrl; + } + + if (url == null) return null; - var parts = url.Substring ("dotnet://".Length).Split ('/'); - if (parts.Length != 2) + var parts = ParseDocumentUrl (url); + if (parts.Assembly == null) return null; var line = args? ["lineNumber"]?.Value<int> (); @@ -37,12 +52,24 @@ namespace WsProxy { return null; return new BreakPointRequest () { - Assembly = parts [0], - File = parts [1], + Assembly = parts.Assembly, + File = parts.DocumentPath, Line = line.Value, Column = column.Value }; } + + static (string Assembly, string DocumentPath) ParseDocumentUrl (string url) + { + if (Uri.TryCreate (url, UriKind.Absolute, out var docUri) && docUri.Scheme == "dotnet") { + return ( + docUri.Host, + docUri.PathAndQuery.Substring (1) + ); + } else { + return (null, null); + } + } } @@ -101,7 +128,7 @@ namespace WsProxy { public SourceLocation (MethodInfo mi, SequencePoint sp) { this.id = mi.SourceId; - this.line = sp.StartLine; + this.line = sp.StartLine - 1; this.column = sp.StartColumn - 1; this.cliLoc = new CliLocation (mi, sp.Offset); } @@ -260,13 +287,13 @@ namespace WsProxy { } } - internal class AssemblyInfo { static int next_id; ModuleDefinition image; readonly int id; Dictionary<int, MethodInfo> methods = new Dictionary<int, MethodInfo> (); - readonly List<SourceFile> sources = new List<SourceFile> (); + Dictionary<string, string> sourceLinkMappings = new Dictionary<string, string>(); + readonly List<SourceFile> sources = new List<SourceFile>(); public AssemblyInfo (byte[] assembly, byte[] pdb) { @@ -274,16 +301,35 @@ namespace WsProxy { this.id = ++next_id; } - ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/); - if (pdb != null) { - rp.ReadSymbols = true; - rp.SymbolReaderProvider = new PortablePdbReaderProvider (); - rp.SymbolStream = new MemoryStream (pdb); + try { + ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/); + if (pdb != null) { + rp.ReadSymbols = true; + rp.SymbolReaderProvider = new PortablePdbReaderProvider (); + rp.SymbolStream = new MemoryStream (pdb); + } + + rp.ReadingMode = ReadingMode.Immediate; + rp.InMemory = true; + + this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp); + } catch (BadImageFormatException ex) { + Console.WriteLine ($"Failed to read assembly as portable PDB: {ex.Message}"); } - rp.InMemory = true; + if (this.image == null) { + ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/); + if (pdb != null) { + rp.ReadSymbols = true; + rp.SymbolReaderProvider = new NativePdbReaderProvider (); + rp.SymbolStream = new MemoryStream (pdb); + } + + rp.ReadingMode = ReadingMode.Immediate; + rp.InMemory = true; - this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp); + this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp); + } Populate (); } @@ -294,6 +340,8 @@ namespace WsProxy { void Populate () { + ProcessSourceLink(); + var d2s = new Dictionary<Document, SourceFile> (); Func<Document, SourceFile> get_src = (doc) => { @@ -301,33 +349,88 @@ namespace WsProxy { return null; if (d2s.ContainsKey (doc)) return d2s [doc]; - var src = new SourceFile (this, sources.Count, doc); + var src = new SourceFile (this, sources.Count, doc, GetSourceLinkUrl (doc.Url)); sources.Add (src); d2s [doc] = src; return src; }; - foreach (var m in image.GetTypes ().SelectMany (t => t.Methods)) { + foreach (var m in image.GetTypes().SelectMany(t => t.Methods)) { Document first_doc = null; foreach (var sp in m.DebugInformation.SequencePoints) { - if (first_doc == null) { + if (first_doc == null && !sp.Document.Url.EndsWith (".g.cs")) { first_doc = sp.Document; - } else if (first_doc != sp.Document) { - //FIXME this is needed for (c)ctors in corlib - throw new Exception ($"Cant handle multi-doc methods in {m}"); } + // else if (first_doc != sp.Document) { + // //FIXME this is needed for (c)ctors in corlib + // throw new Exception ($"Cant handle multi-doc methods in {m}"); + //} + } + + if (first_doc == null) { + // all generated files + first_doc = m.DebugInformation.SequencePoints.FirstOrDefault ()?.Document; + } + + if (first_doc != null) { + var src = get_src (first_doc); + var mi = new MethodInfo (this, m, src); + int mt = (int)m.MetadataToken.RID; + this.methods [mt] = mi; + if (src != null) + src.AddMethod (mi); } + } + } + + private void ProcessSourceLink () + { + var sourceLinkDebugInfo = image.CustomDebugInformations.FirstOrDefault (i => i.Kind == CustomDebugInformationKind.SourceLink); - var src = get_src (first_doc); - var mi = new MethodInfo (this, m, src); - int mt = (int)m.MetadataToken.RID; - this.methods [mt] = mi; - if (src != null) - src.AddMethod (mi); + if (sourceLinkDebugInfo != null) { + var sourceLinkContent = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content; + if (sourceLinkContent != null) { + var jObject = JObject.Parse (sourceLinkContent) ["documents"]; + sourceLinkMappings = JsonConvert.DeserializeObject<Dictionary<string, string>> (jObject.ToString ()); + } } } + private Uri GetSourceLinkUrl (string document) + { + if (sourceLinkMappings.TryGetValue (document, out string url)) { + return new Uri (url); + } + + foreach (var sourceLinkDocument in sourceLinkMappings) { + string key = sourceLinkDocument.Key; + + if (Path.GetFileName (key) != "*") { + continue; + } + + var keyTrim = key.TrimEnd ('*'); + + if (document.StartsWith(keyTrim, StringComparison.OrdinalIgnoreCase)) { + var docUrlPart = document.Replace (keyTrim, ""); + return new Uri (sourceLinkDocument.Value.TrimEnd ('*') + docUrlPart); + } + } + + return null; + } + + private string GetRelativePath (string relativeTo, string path) + { + var uri = new Uri (relativeTo, UriKind.RelativeOrAbsolute); + var rel = Uri.UnescapeDataString (uri.MakeRelativeUri (new Uri (path, UriKind.RelativeOrAbsolute)).ToString ()).Replace (Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + if (rel.Contains (Path.DirectorySeparatorChar.ToString ()) == false) { + rel = $".{ Path.DirectorySeparatorChar }{ rel }"; + } + return rel; + } + public IEnumerable<SourceFile> Sources { get { return this.sources; } } @@ -342,9 +445,9 @@ namespace WsProxy { public MethodInfo GetMethodByToken (int token) { - return methods [token]; + methods.TryGetValue (token, out var value); + return value; } - } internal class SourceFile { @@ -353,23 +456,36 @@ namespace WsProxy { int id; Document doc; - internal SourceFile (AssemblyInfo assembly, int id, Document doc) + internal SourceFile (AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri) { this.methods = new HashSet<MethodInfo> (); + this.SourceLinkUri = sourceLinkUri; this.assembly = assembly; this.id = id; this.doc = doc; + this.DebuggerFileName = doc.Url.Replace ("\\", "/").Replace (":", ""); + + this.SourceUri = new Uri ((Path.IsPathRooted (doc.Url) ? "file://" : "") + doc.Url, UriKind.RelativeOrAbsolute); + if (SourceUri.IsFile && File.Exists (SourceUri.LocalPath)) { + this.Url = this.SourceUri.ToString (); + } else { + this.Url = DotNetUrl; + } + } internal void AddMethod (MethodInfo mi) { this.methods.Add (mi); } - public string FileName => Path.GetFileName (doc.Url); - public string Url => $"dotnet://{assembly.Name}/{FileName}"; + public string DebuggerFileName { get; } + public string Url { get; } + public string AssemblyName => assembly.Name; + public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}"; public string DocHashCode => "abcdee" + id; public SourceId SourceId => new SourceId (assembly.Id, this.id); - public string LocalPath => doc.Url; + public Uri SourceLinkUri { get; } + public Uri SourceUri { get; } public IEnumerable<MethodInfo> Methods => this.methods; } @@ -377,17 +493,18 @@ namespace WsProxy { internal class DebugStore { List<AssemblyInfo> assemblies = new List<AssemblyInfo> (); - public DebugStore (string[] loaded_files) + public DebugStore (string [] loaded_files) { - bool MatchPdb (string asm, string pdb) { + bool MatchPdb (string asm, string pdb) + { return Path.ChangeExtension (asm, "pdb") == pdb; } var asm_files = new List<string> (); var pdb_files = new List<string> (); foreach (var f in loaded_files) { - var file_name = f.ToLower (); - if (file_name.EndsWith (".pdb", StringComparison.Ordinal)) + var file_name = f; + if (file_name.EndsWith (".pdb", StringComparison.OrdinalIgnoreCase)) pdb_files.Add (file_name); else asm_files.Add (file_name); @@ -395,14 +512,18 @@ namespace WsProxy { //FIXME make this parallel foreach (var p in asm_files) { - var pdb = pdb_files.FirstOrDefault (n => MatchPdb (p, n)); - HttpClient h = new HttpClient (); - var assembly_bytes = h.GetByteArrayAsync (p).Result; - byte[] pdb_bytes = null; - if (pdb != null) - pdb_bytes = h.GetByteArrayAsync (pdb).Result; - - this.assemblies.Add (new AssemblyInfo (assembly_bytes, pdb_bytes)); + try { + var pdb = pdb_files.FirstOrDefault (n => MatchPdb (p, n)); + HttpClient h = new HttpClient (); + var assembly_bytes = h.GetByteArrayAsync (p).Result; + byte [] pdb_bytes = null; + if (pdb != null) + pdb_bytes = h.GetByteArrayAsync (pdb).Result; + + this.assemblies.Add (new AssemblyInfo (assembly_bytes, pdb_bytes)); + } catch (Exception e) { + Console.WriteLine ($"Failed to read {p} ({e.Message})"); + } } } @@ -412,7 +533,6 @@ namespace WsProxy { foreach (var s in a.Sources) yield return s; } - } public SourceFile GetFileById (SourceId id) @@ -426,26 +546,23 @@ namespace WsProxy { } /* - Matching logic here is hilarious and it goes like this: - We inject one line at the top of all sources to make it easy to identify them [1]. V8 uses zero based indexing for both line and column. PPDBs uses one based indexing for both line and column. - Which means that: - - for lines, values are already adjusted (v8 numbers come +1 due to the injected line) - - for columns, we need to +1 the v8 numbers - [1] It's so we can deal with the Runtime.compileScript ide cmd */ static bool Match (SequencePoint sp, SourceLocation start, SourceLocation end) { - if (start.Line > sp.StartLine) + var spStart = (Line: sp.StartLine - 1, Column: sp.StartColumn - 1); + var spEnd = (Line: sp.EndLine - 1, Column: sp.EndColumn - 1); + + if (start.Line > spStart.Line) return false; - if ((start.Column + 1) > sp.StartColumn && start.Line == sp.StartLine) + if (start.Column > spStart.Column && start.Line == sp.StartLine) return false; - if (end.Line < sp.EndLine) + if (end.Line < spEnd.Line) return false; - if ((end.Column + 1) < sp.EndColumn && end.Line == sp.EndLine) + if (end.Column < spEnd.Column && end.Line == spEnd.Line) return false; return true; @@ -477,28 +594,24 @@ namespace WsProxy { } /* - Matching logic here is hilarious and it goes like this: - We inject one line at the top of all sources to make it easy to identify them [1]. V8 uses zero based indexing for both line and column. PPDBs uses one based indexing for both line and column. - Which means that: - - for lines, values are already adjusted (v8 numbers come + 1 due to the injected line) - - for columns, we need to +1 the v8 numbers - [1] It's so we can deal with the Runtime.compileScript ide cmd */ static bool Match (SequencePoint sp, int line, int column) { - if (sp.StartLine > line || sp.EndLine < line) + var bp = (line: line + 1, column: column + 1); + + if (sp.StartLine > bp.line || sp.EndLine < bp.line) return false; //Chrome sends a zero column even if getPossibleBreakpoints say something else if (column == 0) return true; - if (sp.StartColumn > (column + 1) && sp.StartLine == line) + if (sp.StartColumn > bp.column && sp.StartLine == bp.line) return false; - if (sp.EndColumn < (column + 1) && sp.EndLine == line) + if (sp.EndColumn < bp.column && sp.EndLine == bp.line) return false; return true; @@ -506,8 +619,11 @@ namespace WsProxy { public SourceLocation FindBestBreakpoint (BreakPointRequest req) { - var asm = this.assemblies.FirstOrDefault (a => a.Name == req.Assembly); - var src = asm.Sources.FirstOrDefault (s => s.FileName == req.File); + var asm = assemblies.FirstOrDefault (a => a.Name.Equals (req.Assembly, StringComparison.OrdinalIgnoreCase)); + var src = asm?.Sources?.FirstOrDefault (s => s.DebuggerFileName.Equals (req.File, StringComparison.OrdinalIgnoreCase)); + + if (src == null) + return null; foreach (var m in src.Methods) { foreach (var sp in m.methodDef.DebugInformation.SequencePoints) { @@ -521,8 +637,15 @@ namespace WsProxy { } public string ToUrl (SourceLocation location) + => location != null ? GetFileById (location.Id).Url : ""; + + public SourceFile GetFileByUrlRegex (string urlRegex) { - return GetFileById (location.Id).Url; + var regex = new Regex (urlRegex); + return AllSources ().FirstOrDefault (file => regex.IsMatch (file.Url.ToString())); } + + public SourceFile GetFileByUrl (string url) + => AllSources ().FirstOrDefault (file => file.Url.ToString() == url); } } diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs index ae6343bca41..6ff776bffe0 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs @@ -8,6 +8,7 @@ using System.Threading; using System.IO; using System.Text; using System.Collections.Generic; +using System.Net; namespace WsProxy { @@ -20,6 +21,8 @@ namespace WsProxy { public const string REMOVE_BREAK_POINT = "MONO.mono_wasm_remove_breakpoint({0})"; public const string GET_LOADED_FILES = "MONO.mono_wasm_get_loaded_files()"; public const string CLEAR_ALL_BREAKPOINTS = "MONO.mono_wasm_clear_all_breakpoints()"; + public const string GET_OBJECT_PROPERTIES = "MONO.mono_wasm_get_object_properties({0})"; + public const string GET_ARRAY_VALUES = "MONO.mono_wasm_get_array_values({0})"; } internal enum MonoErrorCodes { @@ -128,7 +131,7 @@ namespace WsProxy { case "Debugger.getScriptSource": { var script_id = args? ["scriptId"]?.Value<string> (); if (script_id.StartsWith ("dotnet://", StringComparison.InvariantCultureIgnoreCase)) { - OnGetScriptSource (id, script_id, token); + await OnGetScriptSource (id, script_id, token); return true; } @@ -154,7 +157,7 @@ namespace WsProxy { case "Debugger.setBreakpointByUrl": { Info ($"BP req {args}"); - var bp_req = BreakPointRequest.Parse (args); + var bp_req = BreakPointRequest.Parse (args, store); if (bp_req != null) { await SetBreakPoint (id, bp_req, token); return true; @@ -200,7 +203,14 @@ namespace WsProxy { await GetScopeProperties (id, int.Parse (objId.Substring ("dotnet:scope:".Length)), token); return true; } - + if (objId.StartsWith("dotnet:", StringComparison.InvariantCulture)) + { + if (objId.StartsWith("dotnet:object:", StringComparison.InvariantCulture)) + await GetDetails(id, int.Parse(objId.Substring("dotnet:object:".Length)), token, MonoCommands.GET_OBJECT_PROPERTIES); + if (objId.StartsWith("dotnet:array:", StringComparison.InvariantCulture)) + await GetDetails(id, int.Parse(objId.Substring("dotnet:array:".Length)), token, MonoCommands.GET_ARRAY_VALUES); + return true; + } break; } } @@ -213,6 +223,7 @@ namespace WsProxy { Info ("RUNTIME READY, PARTY TIME"); await RuntimeReady (token); await SendCommand ("Debugger.resume", new JObject (), token); + SendEvent ("Mono.runtimeReady", new JObject (), token); } async Task OnBreakPointHit (JObject args, CancellationToken token) @@ -257,9 +268,9 @@ namespace WsProxy { var src = bp == null ? null : store.GetFileById (bp.Location.Id); var callFrames = new List<JObject> (); - foreach (var f in orig_callframes) { - var function_name = f ["functionName"]?.Value<string> (); - var url = f ["url"]?.Value<string> (); + foreach (var frame in orig_callframes) { + var function_name = frame ["functionName"]?.Value<string> (); + var url = frame ["url"]?.Value<string> (); if ("mono_wasm_fire_bp" == function_name || "_mono_wasm_fire_bp" == function_name) { var frames = new List<Frame> (); int frame_id = 0; @@ -271,14 +282,19 @@ namespace WsProxy { var asm = store.GetAssemblyByName (assembly_name); var method = asm.GetMethodByToken (method_token); - var location = method.GetLocationByIl (il_pos); + + if (method == null) { + Info ($"Unable to find il offset: {il_pos} in method token: {method_token} assembly name: {assembly_name}"); + continue; + } + + var location = method?.GetLocationByIl (il_pos); // When hitting a breakpoint on the "IncrementCount" method in the standard // Blazor project template, one of the stack frames is inside mscorlib.dll // and we get location==null for it. It will trigger a NullReferenceException // if we don't skip over that stack frame. - if (location == null) - { + if (location == null) { continue; } @@ -288,7 +304,7 @@ namespace WsProxy { callFrames.Add (JObject.FromObject (new { functionName = method.Name, - + callFrameId = $"dotnet:scope:{frame_id}", functionLocation = method.StartLocation.ToJObject (), location = location.ToJObject (), @@ -300,25 +316,24 @@ namespace WsProxy { type = "local", @object = new { @type = "object", - className = "Object", + className = "Object", description = "Object", - objectId = $"dotnet:scope:{frame_id}" + objectId = $"dotnet:scope:{frame_id}", }, name = method.Name, startLocation = method.StartLocation.ToJObject (), endLocation = method.EndLocation.ToJObject (), - } - }, - - @this = new { - } + }}, + @this = new { } })); ++frame_id; this.current_callstack = frames; + } - } else if (!url.StartsWith ("wasm://wasm/", StringComparison.InvariantCulture)) { - callFrames.Add (f); + } else if (!(function_name.StartsWith ("wasm-function", StringComparison.InvariantCulture) + || url.StartsWith ("wasm://wasm/", StringComparison.InvariantCulture))) { + callFrames.Add (frame); } } @@ -393,6 +408,57 @@ namespace WsProxy { await SendCommand ("Debugger.resume", new JObject (), token); } + async Task GetDetails(int msg_id, int object_id, CancellationToken token, string command) + { + var o = JObject.FromObject(new + { + expression = string.Format(command, object_id), + objectGroup = "mono_debugger", + includeCommandLineAPI = false, + silent = false, + returnByValue = true, + }); + + var res = await SendCommand("Runtime.evaluate", o, token); + + //if we fail we just buble that to the IDE (and let it panic over it) + if (res.IsErr) + { + SendResponse(msg_id, res, token); + return; + } + + var values = res.Value?["result"]?["value"]?.Values<JObject>().ToArray(); + + var var_list = new List<JObject>(); + + // Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously + // results in a "Memory access out of bounds", causing 'values' to be null, + // so skip returning variable values in that case. + for (int i = 0; i < values.Length; i+=2) + { + string fieldName = (string)values[i]["name"]; + if (fieldName.Contains("k__BackingField")){ + fieldName = fieldName.Replace("k__BackingField", ""); + fieldName = fieldName.Replace("<", ""); + fieldName = fieldName.Replace(">", ""); + } + var_list.Add(JObject.FromObject(new + { + name = fieldName, + value = values[i+1]["value"] + })); + + } + o = JObject.FromObject(new + { + result = var_list + }); + + SendResponse(msg_id, Result.Ok(o), token); + } + + async Task GetScopeProperties (int msg_id, int scope_id, CancellationToken token) { var scope = this.current_callstack.FirstOrDefault (s => s.Id == scope_id); @@ -425,6 +491,10 @@ namespace WsProxy { // results in a "Memory access out of bounds", causing 'values' to be null, // so skip returning variable values in that case. for (int i = 0; values != null && i < vars.Length; ++i) { + var value = values [i] ["value"]; + if (((string)value ["description"]) == null) + value ["description"] = value ["value"]?.ToString(); + var_list.Add (JObject.FromObject (new { name = vars [i].Name, value = values [i] ["value"] @@ -485,7 +555,8 @@ namespace WsProxy { url = s.Url, executionContextId = this.ctx_id, hash = s.DocHashCode, - executionContextAuxData = this.aux_ctx_data + executionContextAuxData = this.aux_ctx_data, + dotNetUrl = s.DotNetUrl }); //Debug ($"\tsending {s.Url}"); SendEvent ("Debugger.scriptParsed", ok, token); @@ -640,23 +711,51 @@ namespace WsProxy { } - void OnGetScriptSource (int msg_id, string script_id, CancellationToken token) + async Task OnGetScriptSource (int msg_id, string script_id, CancellationToken token) { var id = new SourceId (script_id); var src_file = store.GetFileById (id); var res = new StringWriter (); - res.WriteLine ($"//dotnet:{id}"); + //res.WriteLine ($"//{id}"); - using (var f = new StreamReader (File.Open (src_file.LocalPath, FileMode.Open))) { - res.Write (f.ReadToEnd ()); - } + try { + var uri = new Uri (src_file.Url); + if (uri.IsFile && File.Exists(uri.LocalPath)) { + using (var f = new StreamReader (File.Open (src_file.SourceUri.LocalPath, FileMode.Open))) { + await res.WriteAsync (await f.ReadToEndAsync ()); + } - var o = JObject.FromObject (new { - scriptSource = res.ToString () - }); + var o = JObject.FromObject (new { + scriptSource = res.ToString () + }); - SendResponse (msg_id, Result.Ok (o), token); + SendResponse (msg_id, Result.Ok (o), token); + } else if(src_file.SourceLinkUri != null) { + var doc = await new WebClient ().DownloadStringTaskAsync (src_file.SourceLinkUri); + await res.WriteAsync (doc); + + var o = JObject.FromObject (new { + scriptSource = res.ToString () + }); + + SendResponse (msg_id, Result.Ok (o), token); + } else { + var o = JObject.FromObject (new { + scriptSource = $"// Unable to find document {src_file.SourceUri}" + }); + + SendResponse (msg_id, Result.Ok (o), token); + } + } catch (Exception e) { + var o = JObject.FromObject (new { + scriptSource = $"// Unable to read document ({e.Message})\n" + + $"Local path: {src_file?.SourceUri}\n" + + $"SourceLink path: {src_file?.SourceLinkUri}\n" + }); + + SendResponse (msg_id, Result.Ok (o), token); + } } } } diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs index 06294916809..e575e0a244b 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs @@ -144,7 +144,7 @@ namespace WsProxy { void Send (WebSocket to, JObject o, CancellationToken token) { - var bytes = Encoding.UTF8.GetBytes (o.ToString ()); + var bytes = Encoding.UTF8.GetBytes (o.ToString ()); var queue = GetQueueForSocket (to); var task = queue.Send (bytes, token); @@ -256,7 +256,7 @@ namespace WsProxy { } // , HttpContext context) - public async Task Run (Uri browserUri, WebSocket ideSocket) + public async Task Run (Uri browserUri, WebSocket ideSocket) { Debug ("wsproxy start"); using (this.ide = ideSocket) { @@ -276,7 +276,7 @@ namespace WsProxy { try { while (!x.IsCancellationRequested) { - var task = await Task.WhenAny (pending_ops); + var task = await Task.WhenAny (pending_ops.ToArray ()); //Console.WriteLine ("pump {0} {1}", task, pending_ops.IndexOf (task)); if (task == pending_ops [0]) { var msg = ((Task<string>)task).Result; -- GitLab