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