Update Mono WebAssembly for Blazor (https://github.com/aspnet/Blazor/pull/1807)

This commit is contained in:
Steve Sanderson 2019-05-23 13:10:53 +01:00 committed by GitHub
parent dbe9ab7dd5
commit 6c5e1690ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 330 additions and 123 deletions

View File

@ -27,6 +27,11 @@
$(RestoreSources); $(RestoreSources);
https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json;
</RestoreSources> </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. --> <!-- In an orchestrated build, this may be overriden to other Azure feeds. -->
<DotNetAssetRootUrl Condition="'$(DotNetAssetRootUrl)'==''">https://dotnetcli.blob.core.windows.net/dotnet/</DotNetAssetRootUrl> <DotNetAssetRootUrl Condition="'$(DotNetAssetRootUrl)'==''">https://dotnetcli.blob.core.windows.net/dotnet/</DotNetAssetRootUrl>

View File

@ -165,7 +165,7 @@
<MicrosoftWebXdtPackageVersion>1.4.0</MicrosoftWebXdtPackageVersion> <MicrosoftWebXdtPackageVersion>1.4.0</MicrosoftWebXdtPackageVersion>
<SystemIdentityModelTokensJwtPackageVersion>5.3.0</SystemIdentityModelTokensJwtPackageVersion> <SystemIdentityModelTokensJwtPackageVersion>5.3.0</SystemIdentityModelTokensJwtPackageVersion>
<!-- Dependencies for Blazor. --> <!-- 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 --> <!-- Packages from 2.1/2.2 branches used for site extension build -->
<MicrosoftAspNetCoreAzureAppServicesSiteExtension21PackageVersion>2.1.1</MicrosoftAspNetCoreAzureAppServicesSiteExtension21PackageVersion> <MicrosoftAspNetCoreAzureAppServicesSiteExtension21PackageVersion>2.1.1</MicrosoftAspNetCoreAzureAppServicesSiteExtension21PackageVersion>
<MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion>2.2.0</MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion> <MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion>2.2.0</MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion>

View File

@ -6,7 +6,7 @@
<PropertyGroup Label="Blazor build outputs"> <PropertyGroup Label="Blazor build outputs">
<MonoLinkerI18NAssemblies>none</MonoLinkerI18NAssemblies> <!-- See Mono linker docs - allows comma-separated values from: none,all,cjk,mideast,other,rare,west --> <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> <BaseBlazorDistPath>dist/</BaseBlazorDistPath>
<BaseBlazorPackageContentOutputPath>$(BaseBlazorDistPath)_content/</BaseBlazorPackageContentOutputPath> <BaseBlazorPackageContentOutputPath>$(BaseBlazorDistPath)_content/</BaseBlazorPackageContentOutputPath>
<BaseBlazorRuntimeOutputPath>$(BaseBlazorDistPath)_framework/</BaseBlazorRuntimeOutputPath> <BaseBlazorRuntimeOutputPath>$(BaseBlazorDistPath)_framework/</BaseBlazorRuntimeOutputPath>

View File

@ -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

View File

@ -6,6 +6,9 @@ using Mono.Cecil.Cil;
using System.Linq; using System.Linq;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System.Net.Http; using System.Net.Http;
using Mono.Cecil.Pdb;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
namespace WsProxy { namespace WsProxy {
internal class BreakPointRequest { internal class BreakPointRequest {
@ -18,17 +21,29 @@ namespace WsProxy {
return $"BreakPointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}"; 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) if (args == null)
return null; return null;
var url = args? ["url"]?.Value<string> (); 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; return null;
var parts = url.Substring ("dotnet://".Length).Split ('/'); var parts = ParseDocumentUrl (url);
if (parts.Length != 2) if (parts.Assembly == null)
return null; return null;
var line = args? ["lineNumber"]?.Value<int> (); var line = args? ["lineNumber"]?.Value<int> ();
@ -37,12 +52,24 @@ namespace WsProxy {
return null; return null;
return new BreakPointRequest () { return new BreakPointRequest () {
Assembly = parts [0], Assembly = parts.Assembly,
File = parts [1], File = parts.DocumentPath,
Line = line.Value, Line = line.Value,
Column = column.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) public SourceLocation (MethodInfo mi, SequencePoint sp)
{ {
this.id = mi.SourceId; this.id = mi.SourceId;
this.line = sp.StartLine; this.line = sp.StartLine - 1;
this.column = sp.StartColumn - 1; this.column = sp.StartColumn - 1;
this.cliLoc = new CliLocation (mi, sp.Offset); this.cliLoc = new CliLocation (mi, sp.Offset);
} }
@ -260,13 +287,13 @@ namespace WsProxy {
} }
} }
internal class AssemblyInfo { internal class AssemblyInfo {
static int next_id; static int next_id;
ModuleDefinition image; ModuleDefinition image;
readonly int id; readonly int id;
Dictionary<int, MethodInfo> methods = new Dictionary<int, MethodInfo> (); 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) public AssemblyInfo (byte[] assembly, byte[] pdb)
{ {
@ -274,16 +301,35 @@ namespace WsProxy {
this.id = ++next_id; this.id = ++next_id;
} }
ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/); try {
if (pdb != null) { ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/);
rp.ReadSymbols = true; if (pdb != null) {
rp.SymbolReaderProvider = new PortablePdbReaderProvider (); rp.ReadSymbols = true;
rp.SymbolStream = new MemoryStream (pdb); 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);
}
this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp); rp.ReadingMode = ReadingMode.Immediate;
rp.InMemory = true;
this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp);
}
Populate (); Populate ();
} }
@ -294,6 +340,8 @@ namespace WsProxy {
void Populate () void Populate ()
{ {
ProcessSourceLink();
var d2s = new Dictionary<Document, SourceFile> (); var d2s = new Dictionary<Document, SourceFile> ();
Func<Document, SourceFile> get_src = (doc) => { Func<Document, SourceFile> get_src = (doc) => {
@ -301,33 +349,88 @@ namespace WsProxy {
return null; return null;
if (d2s.ContainsKey (doc)) if (d2s.ContainsKey (doc))
return d2s [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); sources.Add (src);
d2s [doc] = src; d2s [doc] = src;
return 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; Document first_doc = null;
foreach (var sp in m.DebugInformation.SequencePoints) { 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; 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}");
//}
} }
var src = get_src (first_doc); if (first_doc == null) {
var mi = new MethodInfo (this, m, src); // all generated files
int mt = (int)m.MetadataToken.RID; first_doc = m.DebugInformation.SequencePoints.FirstOrDefault ()?.Document;
this.methods [mt] = mi; }
if (src != null)
src.AddMethod (mi);
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);
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 { public IEnumerable<SourceFile> Sources {
get { return this.sources; } get { return this.sources; }
} }
@ -342,9 +445,9 @@ namespace WsProxy {
public MethodInfo GetMethodByToken (int token) public MethodInfo GetMethodByToken (int token)
{ {
return methods [token]; methods.TryGetValue (token, out var value);
return value;
} }
} }
internal class SourceFile { internal class SourceFile {
@ -353,23 +456,36 @@ namespace WsProxy {
int id; int id;
Document doc; 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.methods = new HashSet<MethodInfo> ();
this.SourceLinkUri = sourceLinkUri;
this.assembly = assembly; this.assembly = assembly;
this.id = id; this.id = id;
this.doc = doc; 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) internal void AddMethod (MethodInfo mi)
{ {
this.methods.Add (mi); this.methods.Add (mi);
} }
public string FileName => Path.GetFileName (doc.Url); public string DebuggerFileName { get; }
public string Url => $"dotnet://{assembly.Name}/{FileName}"; public string Url { get; }
public string AssemblyName => assembly.Name;
public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}";
public string DocHashCode => "abcdee" + id; public string DocHashCode => "abcdee" + id;
public SourceId SourceId => new SourceId (assembly.Id, this.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; public IEnumerable<MethodInfo> Methods => this.methods;
} }
@ -377,17 +493,18 @@ namespace WsProxy {
internal class DebugStore { internal class DebugStore {
List<AssemblyInfo> assemblies = new List<AssemblyInfo> (); 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; return Path.ChangeExtension (asm, "pdb") == pdb;
} }
var asm_files = new List<string> (); var asm_files = new List<string> ();
var pdb_files = new List<string> (); var pdb_files = new List<string> ();
foreach (var f in loaded_files) { foreach (var f in loaded_files) {
var file_name = f.ToLower (); var file_name = f;
if (file_name.EndsWith (".pdb", StringComparison.Ordinal)) if (file_name.EndsWith (".pdb", StringComparison.OrdinalIgnoreCase))
pdb_files.Add (file_name); pdb_files.Add (file_name);
else else
asm_files.Add (file_name); asm_files.Add (file_name);
@ -395,14 +512,18 @@ namespace WsProxy {
//FIXME make this parallel //FIXME make this parallel
foreach (var p in asm_files) { foreach (var p in asm_files) {
var pdb = pdb_files.FirstOrDefault (n => MatchPdb (p, n)); try {
HttpClient h = new HttpClient (); var pdb = pdb_files.FirstOrDefault (n => MatchPdb (p, n));
var assembly_bytes = h.GetByteArrayAsync (p).Result; HttpClient h = new HttpClient ();
byte[] pdb_bytes = null; var assembly_bytes = h.GetByteArrayAsync (p).Result;
if (pdb != null) byte [] pdb_bytes = null;
pdb_bytes = h.GetByteArrayAsync (pdb).Result; if (pdb != null)
pdb_bytes = h.GetByteArrayAsync (pdb).Result;
this.assemblies.Add (new AssemblyInfo (assembly_bytes, pdb_bytes)); 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) foreach (var s in a.Sources)
yield return s; yield return s;
} }
} }
public SourceFile GetFileById (SourceId id) 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. V8 uses zero based indexing for both line and column.
PPDBs uses one 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) 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; return false;
if ((start.Column + 1) > sp.StartColumn && start.Line == sp.StartLine) if (start.Column > spStart.Column && start.Line == sp.StartLine)
return false; return false;
if (end.Line < sp.EndLine) if (end.Line < spEnd.Line)
return false; return false;
if ((end.Column + 1) < sp.EndColumn && end.Line == sp.EndLine) if (end.Column < spEnd.Column && end.Line == spEnd.Line)
return false; return false;
return true; 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. V8 uses zero based indexing for both line and column.
PPDBs uses one 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) 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; return false;
//Chrome sends a zero column even if getPossibleBreakpoints say something else //Chrome sends a zero column even if getPossibleBreakpoints say something else
if (column == 0) if (column == 0)
return true; return true;
if (sp.StartColumn > (column + 1) && sp.StartLine == line) if (sp.StartColumn > bp.column && sp.StartLine == bp.line)
return false; return false;
if (sp.EndColumn < (column + 1) && sp.EndLine == line) if (sp.EndColumn < bp.column && sp.EndLine == bp.line)
return false; return false;
return true; return true;
@ -506,8 +619,11 @@ namespace WsProxy {
public SourceLocation FindBestBreakpoint (BreakPointRequest req) public SourceLocation FindBestBreakpoint (BreakPointRequest req)
{ {
var asm = this.assemblies.FirstOrDefault (a => a.Name == req.Assembly); var asm = assemblies.FirstOrDefault (a => a.Name.Equals (req.Assembly, StringComparison.OrdinalIgnoreCase));
var src = asm.Sources.FirstOrDefault (s => s.FileName == req.File); 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 m in src.Methods) {
foreach (var sp in m.methodDef.DebugInformation.SequencePoints) { foreach (var sp in m.methodDef.DebugInformation.SequencePoints) {
@ -521,8 +637,15 @@ namespace WsProxy {
} }
public string ToUrl (SourceLocation location) 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);
} }
} }

View File

@ -8,6 +8,7 @@ using System.Threading;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
namespace WsProxy { namespace WsProxy {
@ -20,6 +21,8 @@ namespace WsProxy {
public const string REMOVE_BREAK_POINT = "MONO.mono_wasm_remove_breakpoint({0})"; 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 GET_LOADED_FILES = "MONO.mono_wasm_get_loaded_files()";
public const string CLEAR_ALL_BREAKPOINTS = "MONO.mono_wasm_clear_all_breakpoints()"; 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 { internal enum MonoErrorCodes {
@ -128,7 +131,7 @@ namespace WsProxy {
case "Debugger.getScriptSource": { case "Debugger.getScriptSource": {
var script_id = args? ["scriptId"]?.Value<string> (); var script_id = args? ["scriptId"]?.Value<string> ();
if (script_id.StartsWith ("dotnet://", StringComparison.InvariantCultureIgnoreCase)) { if (script_id.StartsWith ("dotnet://", StringComparison.InvariantCultureIgnoreCase)) {
OnGetScriptSource (id, script_id, token); await OnGetScriptSource (id, script_id, token);
return true; return true;
} }
@ -154,7 +157,7 @@ namespace WsProxy {
case "Debugger.setBreakpointByUrl": { case "Debugger.setBreakpointByUrl": {
Info ($"BP req {args}"); Info ($"BP req {args}");
var bp_req = BreakPointRequest.Parse (args); var bp_req = BreakPointRequest.Parse (args, store);
if (bp_req != null) { if (bp_req != null) {
await SetBreakPoint (id, bp_req, token); await SetBreakPoint (id, bp_req, token);
return true; return true;
@ -200,7 +203,14 @@ namespace WsProxy {
await GetScopeProperties (id, int.Parse (objId.Substring ("dotnet:scope:".Length)), token); await GetScopeProperties (id, int.Parse (objId.Substring ("dotnet:scope:".Length)), token);
return true; 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; break;
} }
} }
@ -213,6 +223,7 @@ namespace WsProxy {
Info ("RUNTIME READY, PARTY TIME"); Info ("RUNTIME READY, PARTY TIME");
await RuntimeReady (token); await RuntimeReady (token);
await SendCommand ("Debugger.resume", new JObject (), token); await SendCommand ("Debugger.resume", new JObject (), token);
SendEvent ("Mono.runtimeReady", new JObject (), token);
} }
async Task OnBreakPointHit (JObject args, CancellationToken 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 src = bp == null ? null : store.GetFileById (bp.Location.Id);
var callFrames = new List<JObject> (); var callFrames = new List<JObject> ();
foreach (var f in orig_callframes) { foreach (var frame in orig_callframes) {
var function_name = f ["functionName"]?.Value<string> (); var function_name = frame ["functionName"]?.Value<string> ();
var url = f ["url"]?.Value<string> (); var url = frame ["url"]?.Value<string> ();
if ("mono_wasm_fire_bp" == function_name || "_mono_wasm_fire_bp" == function_name) { if ("mono_wasm_fire_bp" == function_name || "_mono_wasm_fire_bp" == function_name) {
var frames = new List<Frame> (); var frames = new List<Frame> ();
int frame_id = 0; int frame_id = 0;
@ -271,14 +282,19 @@ namespace WsProxy {
var asm = store.GetAssemblyByName (assembly_name); var asm = store.GetAssemblyByName (assembly_name);
var method = asm.GetMethodByToken (method_token); 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 // When hitting a breakpoint on the "IncrementCount" method in the standard
// Blazor project template, one of the stack frames is inside mscorlib.dll // Blazor project template, one of the stack frames is inside mscorlib.dll
// and we get location==null for it. It will trigger a NullReferenceException // and we get location==null for it. It will trigger a NullReferenceException
// if we don't skip over that stack frame. // if we don't skip over that stack frame.
if (location == null) if (location == null) {
{
continue; continue;
} }
@ -288,7 +304,7 @@ namespace WsProxy {
callFrames.Add (JObject.FromObject (new { callFrames.Add (JObject.FromObject (new {
functionName = method.Name, functionName = method.Name,
callFrameId = $"dotnet:scope:{frame_id}",
functionLocation = method.StartLocation.ToJObject (), functionLocation = method.StartLocation.ToJObject (),
location = location.ToJObject (), location = location.ToJObject (),
@ -300,25 +316,24 @@ namespace WsProxy {
type = "local", type = "local",
@object = new { @object = new {
@type = "object", @type = "object",
className = "Object", className = "Object",
description = "Object", description = "Object",
objectId = $"dotnet:scope:{frame_id}" objectId = $"dotnet:scope:{frame_id}",
}, },
name = method.Name, name = method.Name,
startLocation = method.StartLocation.ToJObject (), startLocation = method.StartLocation.ToJObject (),
endLocation = method.EndLocation.ToJObject (), endLocation = method.EndLocation.ToJObject (),
} }},
}, @this = new { }
@this = new {
}
})); }));
++frame_id; ++frame_id;
this.current_callstack = frames; this.current_callstack = frames;
} }
} else if (!url.StartsWith ("wasm://wasm/", StringComparison.InvariantCulture)) { } else if (!(function_name.StartsWith ("wasm-function", StringComparison.InvariantCulture)
callFrames.Add (f); || url.StartsWith ("wasm://wasm/", StringComparison.InvariantCulture))) {
callFrames.Add (frame);
} }
} }
@ -393,6 +408,57 @@ namespace WsProxy {
await SendCommand ("Debugger.resume", new JObject (), token); 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) async Task GetScopeProperties (int msg_id, int scope_id, CancellationToken token)
{ {
var scope = this.current_callstack.FirstOrDefault (s => s.Id == scope_id); 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, // results in a "Memory access out of bounds", causing 'values' to be null,
// so skip returning variable values in that case. // so skip returning variable values in that case.
for (int i = 0; values != null && i < vars.Length; ++i) { 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 { var_list.Add (JObject.FromObject (new {
name = vars [i].Name, name = vars [i].Name,
value = values [i] ["value"] value = values [i] ["value"]
@ -485,7 +555,8 @@ namespace WsProxy {
url = s.Url, url = s.Url,
executionContextId = this.ctx_id, executionContextId = this.ctx_id,
hash = s.DocHashCode, hash = s.DocHashCode,
executionContextAuxData = this.aux_ctx_data executionContextAuxData = this.aux_ctx_data,
dotNetUrl = s.DotNetUrl
}); });
//Debug ($"\tsending {s.Url}"); //Debug ($"\tsending {s.Url}");
SendEvent ("Debugger.scriptParsed", ok, token); 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 id = new SourceId (script_id);
var src_file = store.GetFileById (id); var src_file = store.GetFileById (id);
var res = new StringWriter (); var res = new StringWriter ();
res.WriteLine ($"//dotnet:{id}"); //res.WriteLine ($"//{id}");
using (var f = new StreamReader (File.Open (src_file.LocalPath, FileMode.Open))) { try {
res.Write (f.ReadToEnd ()); 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 ()
});
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);
} }
var o = JObject.FromObject (new {
scriptSource = res.ToString ()
});
SendResponse (msg_id, Result.Ok (o), token);
} }
} }
} }

View File

@ -144,7 +144,7 @@ namespace WsProxy {
void Send (WebSocket to, JObject o, CancellationToken token) 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 queue = GetQueueForSocket (to);
var task = queue.Send (bytes, token); var task = queue.Send (bytes, token);
@ -256,7 +256,7 @@ namespace WsProxy {
} }
// , HttpContext context) // , HttpContext context)
public async Task Run (Uri browserUri, WebSocket ideSocket) public async Task Run (Uri browserUri, WebSocket ideSocket)
{ {
Debug ("wsproxy start"); Debug ("wsproxy start");
using (this.ide = ideSocket) { using (this.ide = ideSocket) {
@ -276,7 +276,7 @@ namespace WsProxy {
try { try {
while (!x.IsCancellationRequested) { 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)); //Console.WriteLine ("pump {0} {1}", task, pending_ops.IndexOf (task));
if (task == pending_ops [0]) { if (task == pending_ops [0]) {
var msg = ((Task<string>)task).Result; var msg = ((Task<string>)task).Result;