Use MonoProxy package for debugging

This commit is contained in:
Safia Abdalla 2020-08-09 11:13:09 -07:00
parent 432f9b8a91
commit 691910c67d
16 changed files with 28 additions and 2801 deletions

View File

@ -124,6 +124,7 @@ and are generated based on the last package release.
<LatestPackageReference Include="Microsoft.Internal.AspNetCore.H2Spec.All" />
<LatestPackageReference Include="Microsoft.Extensions.Internal.Transport" />
<LatestPackageReference Include="Microsoft.NETCore.Windows.ApiSets" />
<LatestPackageReference Include="Microsoft.NETCore.BrowserDebugHost.Transport" />
<LatestPackageReference Include="Microsoft.Owin.Security.Cookies" />
<LatestPackageReference Include="Microsoft.Owin.Testing" />
<LatestPackageReference Include="Microsoft.Web.Administration" />

View File

@ -306,6 +306,10 @@
<Uri>https://github.com/dotnet/runtime</Uri>
<Sha>1dfd9438149f74ae11918a7b0709b8d58c61443f</Sha>
</Dependency>
<Dependency Name="Microsoft.NETCore.BrowserDebugHost.Transport" Version="5.0.0-rc.1.20414.5">
<Uri>https://github.com/dotnet/runtime</Uri>
<Sha>6cc7cffaff2e0413ee84d9a7736f9ae1aa9a3f12</Sha>
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<!-- Listed explicitly to workaround https://github.com/dotnet/cli/issues/10528 -->

View File

@ -1,10 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
{
public class DebugProxyOptions
{
public string BrowserHost { get; set; }
}
}

View File

@ -1,65 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.Hosting
{
public static class DebugProxyHost
{
/// <summary>
/// Creates a custom HostBuilder for the DebugProxyLauncher so that we can inject
/// only the needed configurations.
/// </summary>
/// <param name="args">Command line arguments passed in</param>
/// <param name="browserHost">Host where browser is listening for debug connections</param>
/// <returns><see cref="IHostBuilder" /></returns>
public static IHostBuilder CreateDefaultBuilder(string[] args, string browserHost)
{
var builder = new HostBuilder();
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
if (args != null)
{
config.AddCommandLine(args);
}
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddJsonFile("blazor-debugproxysettings.json", optional: true, reloadOnChange: true);
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
// By default we bind to a dyamic port
// This can be overridden using an option like "--urls http://localhost:9500"
webBuilder.UseUrls("http://127.0.0.1:0");
})
.ConfigureServices(serviceCollection =>
{
serviceCollection.AddSingleton(new DebugProxyOptions
{
BrowserHost = browserHost
});
});
return builder;
}
}
}

View File

@ -1,30 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>
<PackageId>Microsoft.AspNetCore.Components.WebAssembly.DebugProxy</PackageId>
<IsShippingPackage>true</IsShippingPackage>
<Description>Debug proxy for use when building Blazor applications.</Description>
<!-- Set this to false because assemblies should not reference this assembly directly, (except for tests, of course). -->
<IsProjectReferenceProvider>false</IsProjectReferenceProvider>
<NoWarn>$(NoWarn);CS0649</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.WebSockets" />
<Reference Include="Microsoft.Extensions.Hosting" />
<!-- Dependencies of ws-proxy sources -->
<Reference Include="Newtonsoft.Json" />
<Reference Include="Mono.Cecil" />
<Reference Include="Microsoft.CodeAnalysis.CSharp" />
</ItemGroup>
</Project>

View File

@ -1,843 +0,0 @@
using System;
using System.IO;
using System.Collections.Generic;
using Mono.Cecil;
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;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
namespace WebAssembly.Net.Debugging {
internal class BreakpointRequest {
public string Id { get; private set; }
public string Assembly { get; private set; }
public string File { get; private set; }
public int Line { get; private set; }
public int Column { get; private set; }
public MethodInfo Method { get; private set; }
JObject request;
public bool IsResolved => Assembly != null;
public List<Breakpoint> Locations { get; } = new List<Breakpoint> ();
public override string ToString ()
=> $"BreakpointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}";
public object AsSetBreakpointByUrlResponse (IEnumerable<object> jsloc)
=> new { breakpointId = Id, locations = Locations.Select(l => l.Location.AsLocation ()).Concat (jsloc) };
public BreakpointRequest () {
}
public BreakpointRequest (string id, MethodInfo method) {
Id = id;
Method = method;
}
public BreakpointRequest (string id, JObject request) {
Id = id;
this.request = request;
}
public static BreakpointRequest Parse (string id, JObject args)
{
return new BreakpointRequest (id, args);
}
public BreakpointRequest Clone ()
=> new BreakpointRequest { Id = Id, request = request };
public bool IsMatch (SourceFile sourceFile)
{
var url = request? ["url"]?.Value<string> ();
if (url == null) {
var urlRegex = request?["urlRegex"].Value<string>();
var regex = new Regex (urlRegex);
return regex.IsMatch (sourceFile.Url.ToString ()) || regex.IsMatch (sourceFile.DocUrl);
}
return sourceFile.Url.ToString () == url || sourceFile.DotNetUrl == url;
}
public bool TryResolve (SourceFile sourceFile)
{
if (!IsMatch (sourceFile))
return false;
var line = request? ["lineNumber"]?.Value<int> ();
var column = request? ["columnNumber"]?.Value<int> ();
if (line == null || column == null)
return false;
Assembly = sourceFile.AssemblyName;
File = sourceFile.DebuggerFileName;
Line = line.Value;
Column = column.Value;
return true;
}
public bool TryResolve (DebugStore store)
{
if (request == null || store == null)
return false;
return store.AllSources().FirstOrDefault (source => TryResolve (source)) != null;
}
}
internal class VarInfo {
public VarInfo (VariableDebugInformation v)
{
this.Name = v.Name;
this.Index = v.Index;
}
public VarInfo (ParameterDefinition p)
{
this.Name = p.Name;
this.Index = (p.Index + 1) * -1;
}
public string Name { get; }
public int Index { get; }
public override string ToString ()
=> $"(var-info [{Index}] '{Name}')";
}
internal class CliLocation {
public CliLocation (MethodInfo method, int offset)
{
Method = method;
Offset = offset;
}
public MethodInfo Method { get; }
public int Offset { get; }
}
internal class SourceLocation {
SourceId id;
int line;
int column;
CliLocation cliLoc;
public SourceLocation (SourceId id, int line, int column)
{
this.id = id;
this.line = line;
this.column = column;
}
public SourceLocation (MethodInfo mi, SequencePoint sp)
{
this.id = mi.SourceId;
this.line = sp.StartLine - 1;
this.column = sp.StartColumn - 1;
this.cliLoc = new CliLocation (mi, sp.Offset);
}
public SourceId Id { get => id; }
public int Line { get => line; }
public int Column { get => column; }
public CliLocation CliLocation => this.cliLoc;
public override string ToString ()
=> $"{id}:{Line}:{Column}";
public static SourceLocation Parse (JObject obj)
{
if (obj == null)
return null;
if (!SourceId.TryParse (obj ["scriptId"]?.Value<string> (), out var id))
return null;
var line = obj ["lineNumber"]?.Value<int> ();
var column = obj ["columnNumber"]?.Value<int> ();
if (id == null || line == null || column == null)
return null;
return new SourceLocation (id, line.Value, column.Value);
}
internal class LocationComparer : EqualityComparer<SourceLocation>
{
public override bool Equals (SourceLocation l1, SourceLocation l2)
{
if (l1 == null && l2 == null)
return true;
else if (l1 == null || l2 == null)
return false;
return (l1.Line == l2.Line &&
l1.Column == l2.Column &&
l1.Id == l2.Id);
}
public override int GetHashCode (SourceLocation loc)
{
int hCode = loc.Line ^ loc.Column;
return loc.Id.GetHashCode () ^ hCode.GetHashCode ();
}
}
internal object AsLocation ()
=> new {
scriptId = id.ToString (),
lineNumber = line,
columnNumber = column
};
}
internal class SourceId {
const string Scheme = "dotnet://";
readonly int assembly, document;
public int Assembly => assembly;
public int Document => document;
internal SourceId (int assembly, int document)
{
this.assembly = assembly;
this.document = document;
}
public SourceId (string id)
{
if (!TryParse (id, out assembly, out document))
throw new ArgumentException ("invalid source identifier", nameof (id));
}
public static bool TryParse (string id, out SourceId source)
{
source = null;
if (!TryParse (id, out var assembly, out var document))
return false;
source = new SourceId (assembly, document);
return true;
}
static bool TryParse (string id, out int assembly, out int document)
{
assembly = document = 0;
if (id == null || !id.StartsWith (Scheme, StringComparison.Ordinal))
return false;
var sp = id.Substring (Scheme.Length).Split ('_');
if (sp.Length != 2)
return false;
if (!int.TryParse (sp [0], out assembly))
return false;
if (!int.TryParse (sp [1], out document))
return false;
return true;
}
public override string ToString ()
=> $"{Scheme}{assembly}_{document}";
public override bool Equals (object obj)
{
if (obj == null)
return false;
SourceId that = obj as SourceId;
return that.assembly == this.assembly && that.document == this.document;
}
public override int GetHashCode ()
=> assembly.GetHashCode () ^ document.GetHashCode ();
public static bool operator == (SourceId a, SourceId b)
=> ((object)a == null) ? (object)b == null : a.Equals (b);
public static bool operator != (SourceId a, SourceId b)
=> !a.Equals (b);
}
internal class MethodInfo {
MethodDefinition methodDef;
SourceFile source;
public SourceId SourceId => source.SourceId;
public string Name => methodDef.Name;
public MethodDebugInformation DebugInformation => methodDef.DebugInformation;
public SourceLocation StartLocation { get; }
public SourceLocation EndLocation { get; }
public AssemblyInfo Assembly { get; }
public uint Token => methodDef.MetadataToken.RID;
public MethodInfo (AssemblyInfo assembly, MethodDefinition methodDef, SourceFile source)
{
this.Assembly = assembly;
this.methodDef = methodDef;
this.source = source;
var sps = DebugInformation.SequencePoints;
if (sps == null || sps.Count() < 1)
return;
SequencePoint start = sps [0];
SequencePoint end = sps [0];
foreach (var sp in sps) {
if (sp.StartLine < start.StartLine)
start = sp;
else if (sp.StartLine == start.StartLine && sp.StartColumn < start.StartColumn)
start = sp;
if (sp.EndLine > end.EndLine)
end = sp;
else if (sp.EndLine == end.EndLine && sp.EndColumn > end.EndColumn)
end = sp;
}
StartLocation = new SourceLocation (this, start);
EndLocation = new SourceLocation (this, end);
}
public SourceLocation GetLocationByIl (int pos)
{
SequencePoint prev = null;
foreach (var sp in DebugInformation.SequencePoints) {
if (sp.Offset > pos)
break;
prev = sp;
}
if (prev != null)
return new SourceLocation (this, prev);
return null;
}
public VarInfo [] GetLiveVarsAt (int offset)
{
var res = new List<VarInfo> ();
res.AddRange (methodDef.Parameters.Select (p => new VarInfo (p)));
res.AddRange (methodDef.DebugInformation.GetScopes ()
.Where (s => s.Start.Offset <= offset && (s.End.IsEndOfMethod || s.End.Offset > offset))
.SelectMany (s => s.Variables)
.Where (v => !v.IsDebuggerHidden)
.Select (v => new VarInfo (v)));
return res.ToArray ();
}
public override string ToString () => "MethodInfo(" + methodDef.FullName + ")";
}
internal class TypeInfo {
AssemblyInfo assembly;
TypeDefinition type;
List<MethodInfo> methods;
public TypeInfo (AssemblyInfo assembly, TypeDefinition type) {
this.assembly = assembly;
this.type = type;
methods = new List<MethodInfo> ();
}
public string Name => type.Name;
public string FullName => type.FullName;
public List<MethodInfo> Methods => methods;
public override string ToString () => "TypeInfo('" + FullName + "')";
}
class AssemblyInfo {
static int next_id;
ModuleDefinition image;
readonly int id;
readonly ILogger logger;
Dictionary<uint, MethodInfo> methods = new Dictionary<uint, MethodInfo> ();
Dictionary<string, string> sourceLinkMappings = new Dictionary<string, string>();
Dictionary<string, TypeInfo> typesByName = new Dictionary<string, TypeInfo> ();
readonly List<SourceFile> sources = new List<SourceFile>();
internal string Url { get; }
public AssemblyInfo (string url, byte[] assembly, byte[] pdb)
{
this.id = Interlocked.Increment (ref next_id);
try {
Url = url;
ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/);
// set ReadSymbols = true unconditionally in case there
// is an embedded pdb then handle ArgumentException
// and assume that if pdb == null that is the cause
rp.ReadSymbols = true;
rp.SymbolReaderProvider = new PdbReaderProvider ();
if (pdb != null)
rp.SymbolStream = new MemoryStream (pdb);
rp.ReadingMode = ReadingMode.Immediate;
rp.InMemory = true;
this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp);
} catch (BadImageFormatException ex) {
logger.LogWarning ($"Failed to read assembly as portable PDB: {ex.Message}");
} catch (ArgumentException) {
// if pdb == null this is expected and we
// read the assembly without symbols below
if (pdb != null)
throw;
}
if (this.image == null) {
ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/);
if (pdb != null) {
rp.ReadSymbols = true;
rp.SymbolReaderProvider = new PdbReaderProvider ();
rp.SymbolStream = new MemoryStream (pdb);
}
rp.ReadingMode = ReadingMode.Immediate;
rp.InMemory = true;
this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp);
}
Populate ();
}
public AssemblyInfo (ILogger logger)
{
this.logger = logger;
}
void Populate ()
{
ProcessSourceLink();
var d2s = new Dictionary<Document, SourceFile> ();
SourceFile FindSource (Document doc)
{
if (doc == null)
return null;
if (d2s.TryGetValue (doc, out var source))
return source;
var src = new SourceFile (this, sources.Count, doc, GetSourceLinkUrl (doc.Url));
sources.Add (src);
d2s [doc] = src;
return src;
};
foreach (var type in image.GetTypes()) {
var typeInfo = new TypeInfo (this, type);
typesByName [type.FullName] = typeInfo;
foreach (var method in type.Methods) {
foreach (var sp in method.DebugInformation.SequencePoints) {
var source = FindSource (sp.Document);
var methodInfo = new MethodInfo (this, method, source);
methods [method.MetadataToken.RID] = methodInfo;
if (source != null)
source.AddMethod (methodInfo);
typeInfo.Methods.Add (methodInfo);
}
}
}
}
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 static 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
=> this.sources;
public int Id => id;
public string Name => image.Name;
public SourceFile GetDocById (int document)
{
return sources.FirstOrDefault (s => s.SourceId.Document == document);
}
public MethodInfo GetMethodByToken (uint token)
{
methods.TryGetValue (token, out var value);
return value;
}
public TypeInfo GetTypeByName (string name) {
typesByName.TryGetValue (name, out var res);
return res;
}
}
internal class SourceFile {
Dictionary<uint, MethodInfo> methods;
AssemblyInfo assembly;
int id;
Document doc;
internal SourceFile (AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri)
{
this.methods = new Dictionary<uint, 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)
{
if (!this.methods.ContainsKey (mi.Token))
this.methods [mi.Token] = mi;
}
public string DebuggerFileName { get; }
public string Url { get; }
public string AssemblyName => assembly.Name;
public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}";
public SourceId SourceId => new SourceId (assembly.Id, this.id);
public Uri SourceLinkUri { get; }
public Uri SourceUri { get; }
public IEnumerable<MethodInfo> Methods => this.methods.Values;
public string DocUrl => doc.Url;
public (int startLine, int startColumn, int endLine, int endColumn) GetExtents ()
{
var start = Methods.OrderBy (m => m.StartLocation.Line).ThenBy (m => m.StartLocation.Column).First ();
var end = Methods.OrderByDescending (m => m.EndLocation.Line).ThenByDescending (m => m.EndLocation.Column).First ();
return (start.StartLocation.Line, start.StartLocation.Column, end.EndLocation.Line, end.EndLocation.Column);
}
async Task<MemoryStream> GetDataAsync (Uri uri, CancellationToken token)
{
var mem = new MemoryStream ();
try {
if (uri.IsFile && File.Exists (uri.LocalPath)) {
using (var file = File.Open (SourceUri.LocalPath, FileMode.Open)) {
await file.CopyToAsync (mem, token);
mem.Position = 0;
}
} else if (uri.Scheme == "http" || uri.Scheme == "https") {
var client = new HttpClient ();
using (var stream = await client.GetStreamAsync (uri)) {
await stream.CopyToAsync (mem, token);
mem.Position = 0;
}
}
} catch (Exception) {
return null;
}
return mem;
}
static HashAlgorithm GetHashAlgorithm (DocumentHashAlgorithm algorithm)
{
switch (algorithm) {
case DocumentHashAlgorithm.SHA1: return SHA1.Create ();
case DocumentHashAlgorithm.SHA256: return SHA256.Create ();
case DocumentHashAlgorithm.MD5: return MD5.Create ();
}
return null;
}
bool CheckPdbHash (byte [] computedHash)
{
if (computedHash.Length != doc.Hash.Length)
return false;
for (var i = 0; i < computedHash.Length; i++)
if (computedHash[i] != doc.Hash[i])
return false;
return true;
}
byte[] ComputePdbHash (Stream sourceStream)
{
var algorithm = GetHashAlgorithm (doc.HashAlgorithm);
if (algorithm != null)
using (algorithm)
return algorithm.ComputeHash (sourceStream);
return Array.Empty<byte> ();
}
public async Task<Stream> GetSourceAsync (bool checkHash, CancellationToken token = default(CancellationToken))
{
if (doc.EmbeddedSource.Length > 0)
return new MemoryStream (doc.EmbeddedSource, false);
MemoryStream mem;
mem = await GetDataAsync (SourceUri, token);
if (mem != null && (!checkHash || CheckPdbHash (ComputePdbHash (mem)))) {
mem.Position = 0;
return mem;
}
mem = await GetDataAsync (SourceLinkUri, token);
if (mem != null && (!checkHash || CheckPdbHash (ComputePdbHash (mem)))) {
mem.Position = 0;
return mem;
}
return MemoryStream.Null;
}
public object ToScriptSource (int executionContextId, object executionContextAuxData)
{
return new {
scriptId = SourceId.ToString (),
url = Url,
executionContextId,
executionContextAuxData,
//hash: should be the v8 hash algo, managed implementation is pending
dotNetUrl = DotNetUrl,
};
}
}
internal class DebugStore {
List<AssemblyInfo> assemblies = new List<AssemblyInfo> ();
readonly HttpClient client;
readonly ILogger logger;
public DebugStore (ILogger logger, HttpClient client) {
this.client = client;
this.logger = logger;
}
public DebugStore (ILogger logger) : this (logger, new HttpClient ())
{
}
class DebugItem {
public string Url { get; set; }
public Task<byte[][]> Data { get; set; }
}
public async IAsyncEnumerable<SourceFile> Load (SessionId sessionId, string [] loaded_files, [EnumeratorCancellation] CancellationToken token)
{
static bool MatchPdb (string asm, string pdb)
=> Path.ChangeExtension (asm, "pdb") == pdb;
var asm_files = new List<string> ();
var pdb_files = new List<string> ();
foreach (var file_name in loaded_files) {
if (file_name.EndsWith (".pdb", StringComparison.OrdinalIgnoreCase))
pdb_files.Add (file_name);
else
asm_files.Add (file_name);
}
List<DebugItem> steps = new List<DebugItem> ();
foreach (var url in asm_files) {
try {
var pdb = pdb_files.FirstOrDefault (n => MatchPdb (url, n));
steps.Add (
new DebugItem {
Url = url,
Data = Task.WhenAll (client.GetByteArrayAsync (url), pdb != null ? client.GetByteArrayAsync (pdb) : Task.FromResult<byte []> (null))
});
} catch (Exception e) {
logger.LogDebug ($"Failed to read {url} ({e.Message})");
}
}
foreach (var step in steps) {
AssemblyInfo assembly = null;
try {
var bytes = await step.Data;
assembly = new AssemblyInfo (step.Url, bytes [0], bytes [1]);
} catch (Exception e) {
logger.LogDebug ($"Failed to load {step.Url} ({e.Message})");
}
if (assembly == null)
continue;
assemblies.Add (assembly);
foreach (var source in assembly.Sources)
yield return source;
}
}
public IEnumerable<SourceFile> AllSources ()
=> assemblies.SelectMany (a => a.Sources);
public SourceFile GetFileById (SourceId id)
=> AllSources ().SingleOrDefault (f => f.SourceId.Equals (id));
public AssemblyInfo GetAssemblyByName (string name)
=> assemblies.FirstOrDefault (a => a.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase));
/*
V8 uses zero based indexing for both line and column.
PPDBs uses one based indexing for both line and column.
*/
static bool Match (SequencePoint sp, SourceLocation start, SourceLocation end)
{
var spStart = (Line: sp.StartLine - 1, Column: sp.StartColumn - 1);
var spEnd = (Line: sp.EndLine - 1, Column: sp.EndColumn - 1);
if (start.Line > spEnd.Line)
return false;
if (start.Column > spEnd.Column && start.Line == spEnd.Line)
return false;
if (end.Line < spStart.Line)
return false;
if (end.Column < spStart.Column && end.Line == spStart.Line)
return false;
return true;
}
public List<SourceLocation> FindPossibleBreakpoints (SourceLocation start, SourceLocation end)
{
//XXX FIXME no idea what todo with locations on different files
if (start.Id != end.Id) {
logger.LogDebug ($"FindPossibleBreakpoints: documents differ (start: {start.Id}) (end {end.Id}");
return null;
}
var sourceId = start.Id;
var doc = GetFileById (sourceId);
var res = new List<SourceLocation> ();
if (doc == null) {
logger.LogDebug ($"Could not find document {sourceId}");
return res;
}
foreach (var method in doc.Methods) {
foreach (var sequencePoint in method.DebugInformation.SequencePoints) {
if (!sequencePoint.IsHidden && Match (sequencePoint, start, end))
res.Add (new SourceLocation (method, sequencePoint));
}
}
return res;
}
/*
V8 uses zero based indexing for both line and column.
PPDBs uses one based indexing for both line and column.
*/
static bool Match (SequencePoint sp, int line, int column)
{
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 > bp.column && sp.StartLine == bp.line)
return false;
if (sp.EndColumn < bp.column && sp.EndLine == bp.line)
return false;
return true;
}
public IEnumerable<SourceLocation> FindBreakpointLocations (BreakpointRequest request)
{
request.TryResolve (this);
var asm = assemblies.FirstOrDefault (a => a.Name.Equals (request.Assembly, StringComparison.OrdinalIgnoreCase));
var sourceFile = asm?.Sources?.SingleOrDefault (s => s.DebuggerFileName.Equals (request.File, StringComparison.OrdinalIgnoreCase));
if (sourceFile == null)
yield break;
foreach (var method in sourceFile.Methods) {
foreach (var sequencePoint in method.DebugInformation.SequencePoints) {
if (!sequencePoint.IsHidden && Match (sequencePoint, request.Line, request.Column))
yield return new SourceLocation (method, sequencePoint);
}
}
}
public string ToUrl (SourceLocation location)
=> location != null ? GetFileById (location.Id).Url : "";
}
}

View File

@ -1,298 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using System.Threading;
using System.IO;
using System.Collections.Generic;
using System.Net;
using Microsoft.Extensions.Logging;
namespace WebAssembly.Net.Debugging {
internal struct SessionId {
public readonly string sessionId;
public SessionId (string sessionId)
{
this.sessionId = sessionId;
}
// hashset treats 0 as unset
public override int GetHashCode ()
=> sessionId?.GetHashCode () ?? -1;
public override bool Equals (object obj)
=> (obj is SessionId) ? ((SessionId) obj).sessionId == sessionId : false;
public static bool operator == (SessionId a, SessionId b)
=> a.sessionId == b.sessionId;
public static bool operator != (SessionId a, SessionId b)
=> a.sessionId != b.sessionId;
public static SessionId Null { get; } = new SessionId ();
public override string ToString ()
=> $"session-{sessionId}";
}
internal struct MessageId {
public readonly string sessionId;
public readonly int id;
public MessageId (string sessionId, int id)
{
this.sessionId = sessionId;
this.id = id;
}
public static implicit operator SessionId (MessageId id)
=> new SessionId (id.sessionId);
public override string ToString ()
=> $"msg-{sessionId}:::{id}";
public override int GetHashCode ()
=> (sessionId?.GetHashCode () ?? 0) ^ id.GetHashCode ();
public override bool Equals (object obj)
=> (obj is MessageId) ? ((MessageId) obj).sessionId == sessionId && ((MessageId) obj).id == id : false;
}
internal class DotnetObjectId {
public string Scheme { get; }
public string Value { get; }
public static bool TryParse (JToken jToken, out DotnetObjectId objectId)
=> TryParse (jToken?.Value<string>(), out objectId);
public static bool TryParse (string id, out DotnetObjectId objectId)
{
objectId = null;
if (id == null)
return false;
if (!id.StartsWith ("dotnet:"))
return false;
var parts = id.Split (":", 3);
if (parts.Length < 3)
return false;
objectId = new DotnetObjectId (parts[1], parts[2]);
return true;
}
public DotnetObjectId (string scheme, string value)
{
Scheme = scheme;
Value = value;
}
public override string ToString ()
=> $"dotnet:{Scheme}:{Value}";
}
internal struct Result {
public JObject Value { get; private set; }
public JObject Error { get; private set; }
public bool IsOk => Value != null;
public bool IsErr => Error != null;
Result (JObject result, JObject error)
{
if (result != null && error != null)
throw new ArgumentException ($"Both {nameof(result)} and {nameof(error)} arguments cannot be non-null.");
bool resultHasError = String.Compare ((result? ["result"] as JObject)? ["subtype"]?. Value<string> (), "error") == 0;
if (result != null && resultHasError) {
this.Value = null;
this.Error = result;
} else {
this.Value = result;
this.Error = error;
}
}
public static Result FromJson (JObject obj)
{
//Log ("protocol", $"from result: {obj}");
return new Result (obj ["result"] as JObject, obj ["error"] as JObject);
}
public static Result Ok (JObject ok)
=> new Result (ok, null);
public static Result OkFromObject (object ok)
=> Ok (JObject.FromObject(ok));
public static Result Err (JObject err)
=> new Result (null, err);
public static Result Err (string msg)
=> new Result (null, JObject.FromObject (new { message = msg }));
public static Result Exception (Exception e)
=> new Result (null, JObject.FromObject (new { message = e.Message }));
public JObject ToJObject (MessageId target) {
if (IsOk) {
return JObject.FromObject (new {
target.id,
target.sessionId,
result = Value
});
} else {
return JObject.FromObject (new {
target.id,
target.sessionId,
error = Error
});
}
}
public override string ToString ()
{
return $"[Result: IsOk: {IsOk}, IsErr: {IsErr}, Value: {Value?.ToString ()}, Error: {Error?.ToString ()} ]";
}
}
internal class MonoCommands {
public string expression { get; set; }
public string objectGroup { get; set; } = "mono-debugger";
public bool includeCommandLineAPI { get; set; } = false;
public bool silent { get; set; } = false;
public bool returnByValue { get; set; } = true;
public MonoCommands (string expression)
=> this.expression = expression;
public static MonoCommands GetCallStack ()
=> new MonoCommands ("MONO.mono_wasm_get_call_stack()");
public static MonoCommands IsRuntimeReady ()
=> new MonoCommands ("MONO.mono_wasm_runtime_is_ready");
public static MonoCommands StartSingleStepping (StepKind kind)
=> new MonoCommands ($"MONO.mono_wasm_start_single_stepping ({(int)kind})");
public static MonoCommands GetLoadedFiles ()
=> new MonoCommands ("MONO.mono_wasm_get_loaded_files()");
public static MonoCommands ClearAllBreakpoints ()
=> new MonoCommands ("MONO.mono_wasm_clear_all_breakpoints()");
public static MonoCommands GetDetails (DotnetObjectId objectId, JToken args = null)
=> new MonoCommands ($"MONO.mono_wasm_get_details ('{objectId}', {(args ?? "{}")})");
public static MonoCommands GetScopeVariables (int scopeId, params int[] vars)
=> new MonoCommands ($"MONO.mono_wasm_get_variables({scopeId}, [ {string.Join (",", vars)} ])");
public static MonoCommands SetBreakpoint (string assemblyName, uint methodToken, int ilOffset)
=> new MonoCommands ($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})");
public static MonoCommands RemoveBreakpoint (int breakpointId)
=> new MonoCommands ($"MONO.mono_wasm_remove_breakpoint({breakpointId})");
public static MonoCommands ReleaseObject (DotnetObjectId objectId)
=> new MonoCommands ($"MONO.mono_wasm_release_object('{objectId}')");
public static MonoCommands CallFunctionOn (JToken args)
=> new MonoCommands ($"MONO.mono_wasm_call_function_on ({args.ToString ()})");
}
internal enum MonoErrorCodes {
BpNotFound = 100000,
}
internal class MonoConstants {
public const string RUNTIME_IS_READY = "mono_wasm_runtime_ready";
}
class Frame {
public Frame (MethodInfo method, SourceLocation location, int id)
{
this.Method = method;
this.Location = location;
this.Id = id;
}
public MethodInfo Method { get; private set; }
public SourceLocation Location { get; private set; }
public int Id { get; private set; }
}
class Breakpoint {
public SourceLocation Location { get; private set; }
public int RemoteId { get; set; }
public BreakpointState State { get; set; }
public string StackId { get; private set; }
public static bool TryParseId (string stackId, out int id)
{
id = -1;
if (stackId?.StartsWith ("dotnet:", StringComparison.Ordinal) != true)
return false;
return int.TryParse (stackId.Substring ("dotnet:".Length), out id);
}
public Breakpoint (string stackId, SourceLocation loc, BreakpointState state)
{
this.StackId = stackId;
this.Location = loc;
this.State = state;
}
}
enum BreakpointState {
Active,
Disabled,
Pending
}
enum StepKind {
Into,
Out,
Over
}
internal class ExecutionContext {
public string DebuggerId { get; set; }
public Dictionary<string,BreakpointRequest> BreakpointRequests { get; } = new Dictionary<string,BreakpointRequest> ();
public TaskCompletionSource<DebugStore> ready = null;
public bool IsRuntimeReady => ready != null && ready.Task.IsCompleted;
public int Id { get; set; }
public object AuxData { get; set; }
public List<Frame> CallStack { get; set; }
internal DebugStore store;
public TaskCompletionSource<DebugStore> Source { get; } = new TaskCompletionSource<DebugStore> ();
public Dictionary<string, JToken> LocalsCache = new Dictionary<string, JToken> ();
public DebugStore Store {
get {
if (store == null || !Source.Task.IsCompleted)
return null;
return store;
}
}
public void ClearState ()
{
CallStack = null;
LocalsCache.Clear ();
}
}
}

View File

@ -1,337 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.IO;
using System.Text;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
namespace WebAssembly.Net.Debugging {
class DevToolsQueue {
Task current_send;
List<byte []> pending;
public WebSocket Ws { get; private set; }
public Task CurrentSend { get { return current_send; } }
public DevToolsQueue (WebSocket sock)
{
this.Ws = sock;
pending = new List<byte []> ();
}
public Task Send (byte [] bytes, CancellationToken token)
{
pending.Add (bytes);
if (pending.Count == 1) {
if (current_send != null)
throw new Exception ("current_send MUST BE NULL IF THERE'S no pending send");
//logger.LogTrace ("sending {0} bytes", bytes.Length);
current_send = Ws.SendAsync (new ArraySegment<byte> (bytes), WebSocketMessageType.Text, true, token);
return current_send;
}
return null;
}
public Task Pump (CancellationToken token)
{
current_send = null;
pending.RemoveAt (0);
if (pending.Count > 0) {
if (current_send != null)
throw new Exception ("current_send MUST BE NULL IF THERE'S no pending send");
current_send = Ws.SendAsync (new ArraySegment<byte> (pending [0]), WebSocketMessageType.Text, true, token);
return current_send;
}
return null;
}
}
internal class DevToolsProxy {
TaskCompletionSource<bool> side_exception = new TaskCompletionSource<bool> ();
TaskCompletionSource<bool> client_initiated_close = new TaskCompletionSource<bool> ();
Dictionary<MessageId, TaskCompletionSource<Result>> pending_cmds = new Dictionary<MessageId, TaskCompletionSource<Result>> ();
ClientWebSocket browser;
WebSocket ide;
int next_cmd_id;
List<Task> pending_ops = new List<Task> ();
List<DevToolsQueue> queues = new List<DevToolsQueue> ();
protected readonly ILogger logger;
public DevToolsProxy (ILoggerFactory loggerFactory)
{
logger = loggerFactory.CreateLogger<DevToolsProxy>();
}
protected virtual Task<bool> AcceptEvent (SessionId sessionId, string method, JObject args, CancellationToken token)
{
return Task.FromResult (false);
}
protected virtual Task<bool> AcceptCommand (MessageId id, string method, JObject args, CancellationToken token)
{
return Task.FromResult (false);
}
async Task<string> ReadOne (WebSocket socket, CancellationToken token)
{
byte [] buff = new byte [4000];
var mem = new MemoryStream ();
while (true) {
if (socket.State != WebSocketState.Open) {
Log ("error", $"DevToolsProxy: Socket is no longer open.");
client_initiated_close.TrySetResult (true);
return null;
}
var result = await socket.ReceiveAsync (new ArraySegment<byte> (buff), token);
if (result.MessageType == WebSocketMessageType.Close) {
client_initiated_close.TrySetResult (true);
return null;
}
mem.Write (buff, 0, result.Count);
if (result.EndOfMessage)
return Encoding.UTF8.GetString (mem.GetBuffer (), 0, (int)mem.Length);
}
}
DevToolsQueue GetQueueForSocket (WebSocket ws)
{
return queues.FirstOrDefault (q => q.Ws == ws);
}
DevToolsQueue GetQueueForTask (Task task)
{
return queues.FirstOrDefault (q => q.CurrentSend == task);
}
void Send (WebSocket to, JObject o, CancellationToken token)
{
var sender = browser == to ? "Send-browser" : "Send-ide";
var method = o ["method"]?.ToString ();
//if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled")
Log ("protocol", $"{sender}: " + JsonConvert.SerializeObject (o));
var bytes = Encoding.UTF8.GetBytes (o.ToString ());
var queue = GetQueueForSocket (to);
var task = queue.Send (bytes, token);
if (task != null)
pending_ops.Add (task);
}
async Task OnEvent (SessionId sessionId, string method, JObject args, CancellationToken token)
{
try {
if (!await AcceptEvent (sessionId, method, args, token)) {
//logger.LogDebug ("proxy browser: {0}::{1}",method, args);
SendEventInternal (sessionId, method, args, token);
}
} catch (Exception e) {
side_exception.TrySetException (e);
}
}
async Task OnCommand (MessageId id, string method, JObject args, CancellationToken token)
{
try {
if (!await AcceptCommand (id, method, args, token)) {
var res = await SendCommandInternal (id, method, args, token);
SendResponseInternal (id, res, token);
}
} catch (Exception e) {
side_exception.TrySetException (e);
}
}
void OnResponse (MessageId id, Result result)
{
//logger.LogTrace ("got id {0} res {1}", id, result);
// Fixme
if (pending_cmds.Remove (id, out var task)) {
task.SetResult (result);
return;
}
logger.LogError ("Cannot respond to command: {id} with result: {result} - command is not pending", id, result);
}
void ProcessBrowserMessage (string msg, CancellationToken token)
{
var res = JObject.Parse (msg);
var method = res ["method"]?.ToString ();
//if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled")
Log ("protocol", $"browser: {msg}");
if (res ["id"] == null)
pending_ops.Add (OnEvent (new SessionId (res ["sessionId"]?.Value<string> ()), res ["method"].Value<string> (), res ["params"] as JObject, token));
else
OnResponse (new MessageId (res ["sessionId"]?.Value<string> (), res ["id"].Value<int> ()), Result.FromJson (res));
}
void ProcessIdeMessage (string msg, CancellationToken token)
{
Log ("protocol", $"ide: {msg}");
if (!string.IsNullOrEmpty (msg)) {
var res = JObject.Parse (msg);
pending_ops.Add (OnCommand (
new MessageId (res ["sessionId"]?.Value<string> (), res ["id"].Value<int> ()),
res ["method"].Value<string> (),
res ["params"] as JObject, token));
}
}
internal async Task<Result> SendCommand (SessionId id, string method, JObject args, CancellationToken token) {
//Log ("verbose", $"sending command {method}: {args}");
return await SendCommandInternal (id, method, args, token);
}
Task<Result> SendCommandInternal (SessionId sessionId, string method, JObject args, CancellationToken token)
{
int id = Interlocked.Increment (ref next_cmd_id);
var o = JObject.FromObject (new {
id,
method,
@params = args
});
if (sessionId.sessionId != null)
o["sessionId"] = sessionId.sessionId;
var tcs = new TaskCompletionSource<Result> ();
var msgId = new MessageId (sessionId.sessionId, id);
//Log ("verbose", $"add cmd id {sessionId}-{id}");
pending_cmds[msgId] = tcs;
Send (this.browser, o, token);
return tcs.Task;
}
public void SendEvent (SessionId sessionId, string method, JObject args, CancellationToken token)
{
//Log ("verbose", $"sending event {method}: {args}");
SendEventInternal (sessionId, method, args, token);
}
void SendEventInternal (SessionId sessionId, string method, JObject args, CancellationToken token)
{
var o = JObject.FromObject (new {
method,
@params = args
});
if (sessionId.sessionId != null)
o["sessionId"] = sessionId.sessionId;
Send (this.ide, o, token);
}
internal void SendResponse (MessageId id, Result result, CancellationToken token)
{
SendResponseInternal (id, result, token);
}
void SendResponseInternal (MessageId id, Result result, CancellationToken token)
{
JObject o = result.ToJObject (id);
if (result.IsErr)
logger.LogError ($"sending error response for id: {id} -> {result}");
Send (this.ide, o, token);
}
// , HttpContext context)
public async Task Run (Uri browserUri, WebSocket ideSocket)
{
Log ("info", $"DevToolsProxy: Starting on {browserUri}");
using (this.ide = ideSocket) {
Log ("verbose", $"DevToolsProxy: IDE waiting for connection on {browserUri}");
queues.Add (new DevToolsQueue (this.ide));
using (this.browser = new ClientWebSocket ()) {
this.browser.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan;
await this.browser.ConnectAsync (browserUri, CancellationToken.None);
queues.Add (new DevToolsQueue (this.browser));
Log ("verbose", $"DevToolsProxy: Client connected on {browserUri}");
var x = new CancellationTokenSource ();
pending_ops.Add (ReadOne (browser, x.Token));
pending_ops.Add (ReadOne (ide, x.Token));
pending_ops.Add (side_exception.Task);
pending_ops.Add (client_initiated_close.Task);
try {
while (!x.IsCancellationRequested) {
var task = await Task.WhenAny (pending_ops.ToArray ());
//logger.LogTrace ("pump {0} {1}", task, pending_ops.IndexOf (task));
if (task == pending_ops [0]) {
var msg = ((Task<string>)task).Result;
if (msg != null) {
pending_ops [0] = ReadOne (browser, x.Token); //queue next read
ProcessBrowserMessage (msg, x.Token);
}
} else if (task == pending_ops [1]) {
var msg = ((Task<string>)task).Result;
if (msg != null) {
pending_ops [1] = ReadOne (ide, x.Token); //queue next read
ProcessIdeMessage (msg, x.Token);
}
} else if (task == pending_ops [2]) {
var res = ((Task<bool>)task).Result;
throw new Exception ("side task must always complete with an exception, what's going on???");
} else if (task == pending_ops [3]) {
var res = ((Task<bool>)task).Result;
Log ("verbose", $"DevToolsProxy: Client initiated close from {browserUri}");
x.Cancel ();
} else {
//must be a background task
pending_ops.Remove (task);
var queue = GetQueueForTask (task);
if (queue != null) {
var tsk = queue.Pump (x.Token);
if (tsk != null)
pending_ops.Add (tsk);
}
}
}
} catch (Exception e) {
Log ("error", $"DevToolsProxy::Run: Exception {e}");
//throw;
} finally {
if (!x.IsCancellationRequested)
x.Cancel ();
}
}
}
}
protected void Log (string priority, string msg)
{
switch (priority) {
case "protocol":
logger.LogTrace (msg);
break;
case "verbose":
logger.LogDebug (msg);
break;
case "info":
case "warning":
case "error":
default:
logger.LogDebug (msg);
break;
}
}
}
}

View File

@ -1,182 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using System.Threading;
using System.IO;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using System.Reflection;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace WebAssembly.Net.Debugging {
internal class EvaluateExpression {
class FindThisExpression : CSharpSyntaxWalker {
public List<string> thisExpressions = new List<string> ();
public SyntaxTree syntaxTree;
public FindThisExpression (SyntaxTree syntax)
{
syntaxTree = syntax;
}
public override void Visit (SyntaxNode node)
{
if (node is ThisExpressionSyntax) {
if (node.Parent is MemberAccessExpressionSyntax thisParent && thisParent.Name is IdentifierNameSyntax) {
IdentifierNameSyntax var = thisParent.Name as IdentifierNameSyntax;
thisExpressions.Add(var.Identifier.Text);
var newRoot = syntaxTree.GetRoot ().ReplaceNode (node.Parent, thisParent.Name);
syntaxTree = syntaxTree.WithRootAndOptions (newRoot, syntaxTree.Options);
this.Visit (GetExpressionFromSyntaxTree(syntaxTree));
}
}
else
base.Visit (node);
}
public async Task CheckIfIsProperty (MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token)
{
foreach (var var in thisExpressions) {
JToken value = await proxy.TryGetVariableValue (msg_id, scope_id, var, true, token);
if (value == null)
throw new Exception ($"The property {var} does not exist in the current context");
}
}
}
class FindVariableNMethodCall : CSharpSyntaxWalker {
public List<IdentifierNameSyntax> variables = new List<IdentifierNameSyntax> ();
public List<ThisExpressionSyntax> thisList = new List<ThisExpressionSyntax> ();
public List<InvocationExpressionSyntax> methodCall = new List<InvocationExpressionSyntax> ();
public List<object> values = new List<Object> ();
public override void Visit (SyntaxNode node)
{
if (node is IdentifierNameSyntax identifier && !variables.Any (x => x.Identifier.Text == identifier.Identifier.Text))
variables.Add (identifier);
if (node is InvocationExpressionSyntax) {
methodCall.Add (node as InvocationExpressionSyntax);
throw new Exception ("Method Call is not implemented yet");
}
if (node is AssignmentExpressionSyntax)
throw new Exception ("Assignment is not implemented yet");
base.Visit (node);
}
public async Task<SyntaxTree> ReplaceVars (SyntaxTree syntaxTree, MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token)
{
CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot ();
foreach (var var in variables) {
ClassDeclarationSyntax classDeclaration = root.Members.ElementAt (0) as ClassDeclarationSyntax;
MethodDeclarationSyntax method = classDeclaration.Members.ElementAt (0) as MethodDeclarationSyntax;
JToken value = await proxy.TryGetVariableValue (msg_id, scope_id, var.Identifier.Text, false, token);
if (value == null)
throw new Exception ($"The name {var.Identifier.Text} does not exist in the current context");
values.Add (ConvertJSToCSharpType (value ["value"] ["value"].ToString (), value ["value"] ["type"].ToString ()));
var updatedMethod = method.AddParameterListParameters (
SyntaxFactory.Parameter (
SyntaxFactory.Identifier (var.Identifier.Text))
.WithType (SyntaxFactory.ParseTypeName (GetTypeFullName(value["value"]["type"].ToString()))));
root = root.ReplaceNode (method, updatedMethod);
}
syntaxTree = syntaxTree.WithRootAndOptions (root, syntaxTree.Options);
return syntaxTree;
}
private object ConvertJSToCSharpType (string v, string type)
{
switch (type) {
case "number":
return Convert.ChangeType (v, typeof (int));
case "string":
return v;
}
throw new Exception ($"Evaluate of this datatype {type} not implemented yet");
}
private string GetTypeFullName (string type)
{
switch (type) {
case "number":
return typeof (int).FullName;
case "string":
return typeof (string).FullName;
}
throw new Exception ($"Evaluate of this datatype {type} not implemented yet");
}
}
static SyntaxNode GetExpressionFromSyntaxTree (SyntaxTree syntaxTree)
{
CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot ();
ClassDeclarationSyntax classDeclaration = root.Members.ElementAt (0) as ClassDeclarationSyntax;
MethodDeclarationSyntax methodDeclaration = classDeclaration.Members.ElementAt (0) as MethodDeclarationSyntax;
BlockSyntax blockValue = methodDeclaration.Body;
ReturnStatementSyntax returnValue = blockValue.Statements.ElementAt (0) as ReturnStatementSyntax;
InvocationExpressionSyntax expressionInvocation = returnValue.Expression as InvocationExpressionSyntax;
MemberAccessExpressionSyntax expressionMember = expressionInvocation.Expression as MemberAccessExpressionSyntax;
ParenthesizedExpressionSyntax expressionParenthesized = expressionMember.Expression as ParenthesizedExpressionSyntax;
return expressionParenthesized.Expression;
}
internal static async Task<string> CompileAndRunTheExpression (MonoProxy proxy, MessageId msg_id, int scope_id, string expression, CancellationToken token)
{
FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall ();
string retString;
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText (@"
using System;
public class CompileAndRunTheExpression
{
public string Evaluate()
{
return (" + expression + @").ToString();
}
}");
FindThisExpression findThisExpression = new FindThisExpression (syntaxTree);
var expressionTree = GetExpressionFromSyntaxTree(syntaxTree);
findThisExpression.Visit (expressionTree);
await findThisExpression.CheckIfIsProperty (proxy, msg_id, scope_id, token);
syntaxTree = findThisExpression.syntaxTree;
expressionTree = GetExpressionFromSyntaxTree (syntaxTree);
findVarNMethodCall.Visit (expressionTree);
syntaxTree = await findVarNMethodCall.ReplaceVars (syntaxTree, proxy, msg_id, scope_id, token);
MetadataReference [] references = new MetadataReference []
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)
};
CSharpCompilation compilation = CSharpCompilation.Create (
"compileAndRunTheExpression",
syntaxTrees: new [] { syntaxTree },
references: references,
options: new CSharpCompilationOptions (OutputKind.DynamicallyLinkedLibrary));
using (var ms = new MemoryStream ()) {
EmitResult result = compilation.Emit (ms);
ms.Seek (0, SeekOrigin.Begin);
Assembly assembly = Assembly.Load (ms.ToArray ());
Type type = assembly.GetType ("CompileAndRunTheExpression");
object obj = Activator.CreateInstance (type);
var ret = type.InvokeMember ("Evaluate",
BindingFlags.Default | BindingFlags.InvokeMethod,
null,
obj,
//new object [] { 10 }
findVarNMethodCall.values.ToArray ());
retString = ret.ToString ();
}
return retString;
}
}
}

View File

@ -1,885 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using System.Threading;
using System.IO;
using System.Collections.Generic;
using System.Net;
using Microsoft.Extensions.Logging;
using Microsoft.CodeAnalysis;
namespace WebAssembly.Net.Debugging {
internal class MonoProxy : DevToolsProxy {
HashSet<SessionId> sessions = new HashSet<SessionId> ();
Dictionary<SessionId, ExecutionContext> contexts = new Dictionary<SessionId, ExecutionContext> ();
public MonoProxy (ILoggerFactory loggerFactory, bool hideWebDriver = true) : base(loggerFactory) { this.hideWebDriver = hideWebDriver; }
readonly bool hideWebDriver;
internal ExecutionContext GetContext (SessionId sessionId)
{
if (contexts.TryGetValue (sessionId, out var context))
return context;
throw new ArgumentException ($"Invalid Session: \"{sessionId}\"", nameof (sessionId));
}
bool UpdateContext (SessionId sessionId, ExecutionContext executionContext, out ExecutionContext previousExecutionContext)
{
var previous = contexts.TryGetValue (sessionId, out previousExecutionContext);
contexts[sessionId] = executionContext;
return previous;
}
internal Task<Result> SendMonoCommand (SessionId id, MonoCommands cmd, CancellationToken token)
=> SendCommand (id, "Runtime.evaluate", JObject.FromObject (cmd), token);
protected override async Task<bool> AcceptEvent (SessionId sessionId, string method, JObject args, CancellationToken token)
{
switch (method) {
case "Runtime.consoleAPICalled": {
var type = args["type"]?.ToString ();
if (type == "debug") {
if (args["args"]?[0]?["value"]?.ToString () == MonoConstants.RUNTIME_IS_READY && args["args"]?[1]?["value"]?.ToString () == "fe00e07a-5519-4dfe-b35a-f867dbaf2e28")
await RuntimeReady (sessionId, token);
}
break;
}
case "Runtime.executionContextCreated": {
SendEvent (sessionId, method, args, token);
var ctx = args? ["context"];
var aux_data = ctx? ["auxData"] as JObject;
var id = ctx ["id"].Value<int> ();
if (aux_data != null) {
var is_default = aux_data ["isDefault"]?.Value<bool> ();
if (is_default == true) {
await OnDefaultContext (sessionId, new ExecutionContext { Id = id, AuxData = aux_data }, token);
}
}
return true;
}
case "Debugger.paused": {
//TODO figure out how to stich out more frames and, in particular what happens when real wasm is on the stack
var top_func = args? ["callFrames"]? [0]? ["functionName"]?.Value<string> ();
if (top_func == "mono_wasm_fire_bp" || top_func == "_mono_wasm_fire_bp") {
return await OnBreakpointHit (sessionId, args, token);
}
break;
}
case "Debugger.breakpointResolved": {
break;
}
case "Debugger.scriptParsed": {
var url = args? ["url"]?.Value<string> () ?? "";
switch (url) {
case var _ when url == "":
case var _ when url.StartsWith ("wasm://", StringComparison.Ordinal): {
Log ("verbose", $"ignoring wasm: Debugger.scriptParsed {url}");
return true;
}
}
Log ("verbose", $"proxying Debugger.scriptParsed ({sessionId.sessionId}) {url} {args}");
break;
}
case "Target.attachedToTarget": {
if (args["targetInfo"]["type"]?.ToString() == "page")
await DeleteWebDriver (new SessionId (args["sessionId"]?.ToString ()), token);
break;
}
}
return false;
}
async Task<bool> IsRuntimeAlreadyReadyAlready (SessionId sessionId, CancellationToken token)
{
var res = await SendMonoCommand (sessionId, MonoCommands.IsRuntimeReady (), token);
return res.Value? ["result"]? ["value"]?.Value<bool> () ?? false;
}
static int bpIdGenerator;
protected override async Task<bool> AcceptCommand (MessageId id, string method, JObject args, CancellationToken token)
{
// Inspector doesn't use the Target domain or sessions
// so we try to init immediately
if (hideWebDriver && id == SessionId.Null)
await DeleteWebDriver (id, token);
if (!contexts.TryGetValue (id, out var context))
return false;
switch (method) {
case "Target.attachToTarget": {
var resp = await SendCommand (id, method, args, token);
await DeleteWebDriver (new SessionId (resp.Value ["sessionId"]?.ToString ()), token);
break;
}
case "Debugger.enable": {
var resp = await SendCommand (id, method, args, token);
context.DebuggerId = resp.Value ["debuggerId"]?.ToString ();
if (await IsRuntimeAlreadyReadyAlready (id, token))
await RuntimeReady (id, token);
SendResponse (id,resp,token);
return true;
}
case "Debugger.getScriptSource": {
var script = args? ["scriptId"]?.Value<string> ();
return await OnGetScriptSource (id, script, token);
}
case "Runtime.compileScript": {
var exp = args? ["expression"]?.Value<string> ();
if (exp.StartsWith ("//dotnet:", StringComparison.Ordinal)) {
OnCompileDotnetScript (id, token);
return true;
}
break;
}
case "Debugger.getPossibleBreakpoints": {
var resp = await SendCommand (id, method, args, token);
if (resp.IsOk && resp.Value["locations"].HasValues) {
SendResponse (id, resp, token);
return true;
}
var start = SourceLocation.Parse (args? ["start"] as JObject);
//FIXME support variant where restrictToFunction=true and end is omitted
var end = SourceLocation.Parse (args? ["end"] as JObject);
if (start != null && end != null && await GetPossibleBreakpoints (id, start, end, token))
return true;
SendResponse (id, resp, token);
return true;
}
case "Debugger.setBreakpoint": {
break;
}
case "Debugger.setBreakpointByUrl": {
var resp = await SendCommand (id, method, args, token);
if (!resp.IsOk) {
SendResponse (id, resp, token);
return true;
}
var bpid = resp.Value["breakpointId"]?.ToString ();
var locations = resp.Value["locations"]?.Values<object>();
var request = BreakpointRequest.Parse (bpid, args);
// is the store done loading?
var loaded = context.Source.Task.IsCompleted;
if (!loaded) {
// Send and empty response immediately if not
// and register the breakpoint for resolution
context.BreakpointRequests [bpid] = request;
SendResponse (id, resp, token);
}
if (await IsRuntimeAlreadyReadyAlready (id, token)) {
var store = await RuntimeReady (id, token);
Log ("verbose", $"BP req {args}");
await SetBreakpoint (id, store, request, !loaded, token);
}
if (loaded) {
// we were already loaded so we should send a response
// with the locations included and register the request
context.BreakpointRequests [bpid] = request;
var result = Result.OkFromObject (request.AsSetBreakpointByUrlResponse (locations));
SendResponse (id, result, token);
}
return true;
}
case "Debugger.removeBreakpoint": {
await RemoveBreakpoint (id, args, token);
break;
}
case "Debugger.resume": {
await OnResume (id, token);
break;
}
case "Debugger.stepInto": {
return await Step (id, StepKind.Into, token);
}
case "Debugger.stepOut": {
return await Step (id, StepKind.Out, token);
}
case "Debugger.stepOver": {
return await Step (id, StepKind.Over, token);
}
case "Debugger.evaluateOnCallFrame": {
if (!DotnetObjectId.TryParse (args? ["callFrameId"], out var objectId))
return false;
switch (objectId.Scheme) {
case "scope":
return await OnEvaluateOnCallFrame (id,
int.Parse (objectId.Value),
args? ["expression"]?.Value<string> (), token);
default:
return false;
}
}
case "Runtime.getProperties": {
if (!DotnetObjectId.TryParse (args? ["objectId"], out var objectId))
break;
var result = await RuntimeGetProperties (id, objectId, args, token);
SendResponse (id, result, token);
return true;
}
case "Runtime.releaseObject": {
if (!(DotnetObjectId.TryParse (args ["objectId"], out var objectId) && objectId.Scheme == "cfo_res"))
break;
await SendMonoCommand (id, MonoCommands.ReleaseObject (objectId), token);
SendResponse (id, Result.OkFromObject (new{}), token);
return true;
}
// Protocol extensions
case "Dotnet-test.setBreakpointByMethod": {
Console.WriteLine ("set-breakpoint-by-method: " + id + " " + args);
var store = await RuntimeReady (id, token);
string aname = args ["assemblyName"]?.Value<string> ();
string typeName = args ["typeName"]?.Value<string> ();
string methodName = args ["methodName"]?.Value<string> ();
if (aname == null || typeName == null || methodName == null) {
SendResponse (id, Result.Err ("Invalid protocol message '" + args + "'."), token);
return true;
}
// GetAssemblyByName seems to work on file names
var assembly = store.GetAssemblyByName (aname);
if (assembly == null)
assembly = store.GetAssemblyByName (aname + ".exe");
if (assembly == null)
assembly = store.GetAssemblyByName (aname + ".dll");
if (assembly == null) {
SendResponse (id, Result.Err ("Assembly '" + aname + "' not found."), token);
return true;
}
var type = assembly.GetTypeByName (typeName);
if (type == null) {
SendResponse (id, Result.Err ($"Type '{typeName}' not found."), token);
return true;
}
var methodInfo = type.Methods.FirstOrDefault (m => m.Name == methodName);
if (methodInfo == null) {
SendResponse (id, Result.Err ($"Method '{typeName}:{methodName}' not found."), token);
return true;
}
bpIdGenerator ++;
string bpid = "by-method-" + bpIdGenerator.ToString ();
var request = new BreakpointRequest (bpid, methodInfo);
context.BreakpointRequests[bpid] = request;
var loc = methodInfo.StartLocation;
var bp = await SetMonoBreakpoint (id, bpid, loc, token);
if (bp.State != BreakpointState.Active) {
// FIXME:
throw new NotImplementedException ();
}
var resolvedLocation = new {
breakpointId = bpid,
location = loc.AsLocation ()
};
SendEvent (id, "Debugger.breakpointResolved", JObject.FromObject (resolvedLocation), token);
SendResponse (id, Result.OkFromObject (new {
result = new { breakpointId = bpid, locations = new object [] { loc.AsLocation () }}
}), token);
return true;
}
case "Runtime.callFunctionOn": {
if (!DotnetObjectId.TryParse (args ["objectId"], out var objectId))
return false;
var silent = args ["silent"]?.Value<bool> () ?? false;
if (objectId.Scheme == "scope") {
var fail = silent ? Result.OkFromObject (new { result = new { } }) : Result.Exception (new ArgumentException ($"Runtime.callFunctionOn not supported with scope ({objectId})."));
SendResponse (id, fail, token);
return true;
}
var returnByValue = args ["returnByValue"]?.Value<bool> () ?? false;
var res = await SendMonoCommand (id, MonoCommands.CallFunctionOn (args), token);
if (!returnByValue &&
DotnetObjectId.TryParse (res.Value?["result"]?["value"]?["objectId"], out var resultObjectId) &&
resultObjectId.Scheme == "cfo_res")
res = Result.OkFromObject (new { result = res.Value ["result"]["value"] });
if (res.IsErr && silent)
res = Result.OkFromObject (new { result = new { } });
SendResponse (id, res, token);
return true;
}
}
return false;
}
async Task<Result> RuntimeGetProperties (MessageId id, DotnetObjectId objectId, JToken args, CancellationToken token)
{
if (objectId.Scheme == "scope")
return await GetScopeProperties (id, int.Parse (objectId.Value), token);
var res = await SendMonoCommand (id, MonoCommands.GetDetails (objectId, args), token);
if (res.IsErr)
return res;
if (objectId.Scheme == "cfo_res") {
// Runtime.callFunctionOn result object
var value_json_str = res.Value ["result"]?["value"]?["__value_as_json_string__"]?.Value<string> ();
if (value_json_str != null) {
res = Result.OkFromObject (new {
result = JArray.Parse (value_json_str.Replace (@"\""", "\""))
});
} else {
res = Result.OkFromObject (new { result = new {} });
}
} else {
res = Result.Ok (JObject.FromObject (new { result = res.Value ["result"] ["value"] }));
}
return res;
}
//static int frame_id=0;
async Task<bool> OnBreakpointHit (SessionId sessionId, JObject args, CancellationToken token)
{
//FIXME we should send release objects every now and then? Or intercept those we inject and deal in the runtime
var res = await SendMonoCommand (sessionId, MonoCommands.GetCallStack(), token);
var orig_callframes = args? ["callFrames"]?.Values<JObject> ();
var context = GetContext (sessionId);
if (res.IsErr) {
//Give up and send the original call stack
return false;
}
//step one, figure out where did we hit
var res_value = res.Value? ["result"]? ["value"];
if (res_value == null || res_value is JValue) {
//Give up and send the original call stack
return false;
}
Log ("verbose", $"call stack (err is {res.Error} value is:\n{res.Value}");
var bp_id = res_value? ["breakpoint_id"]?.Value<int> ();
Log ("verbose", $"We just hit bp {bp_id}");
if (!bp_id.HasValue) {
//Give up and send the original call stack
return false;
}
var bp = context.BreakpointRequests.Values.SelectMany (v => v.Locations).FirstOrDefault (b => b.RemoteId == bp_id.Value);
var callFrames = new List<object> ();
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;
var the_mono_frames = res.Value? ["result"]? ["value"]? ["frames"]?.Values<JObject> ();
foreach (var mono_frame in the_mono_frames) {
++frame_id;
var il_pos = mono_frame ["il_pos"].Value<int> ();
var method_token = mono_frame ["method_token"].Value<uint> ();
var assembly_name = mono_frame ["assembly_name"].Value<string> ();
// This can be different than `method.Name`, like in case of generic methods
var method_name = mono_frame ["method_name"]?.Value<string> ();
var store = await LoadStore (sessionId, token);
var asm = store.GetAssemblyByName (assembly_name);
if (asm == null) {
Log ("info",$"Unable to find assembly: {assembly_name}");
continue;
}
var method = asm.GetMethodByToken (method_token);
if (method == null) {
Log ("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) {
continue;
}
Log ("info", $"frame il offset: {il_pos} method token: {method_token} assembly name: {assembly_name}");
Log ("info", $"\tmethod {method_name} location: {location}");
frames.Add (new Frame (method, location, frame_id-1));
callFrames.Add (new {
functionName = method_name,
callFrameId = $"dotnet:scope:{frame_id-1}",
functionLocation = method.StartLocation.AsLocation (),
location = location.AsLocation (),
url = store.ToUrl (location),
scopeChain = new [] {
new {
type = "local",
@object = new {
@type = "object",
className = "Object",
description = "Object",
objectId = $"dotnet:scope:{frame_id-1}",
},
name = method_name,
startLocation = method.StartLocation.AsLocation (),
endLocation = method.EndLocation.AsLocation (),
}}
});
context.CallStack = frames;
}
} else if (!(function_name.StartsWith ("wasm-function", StringComparison.Ordinal)
|| url.StartsWith ("wasm://wasm/", StringComparison.Ordinal))) {
callFrames.Add (frame);
}
}
var bp_list = new string [bp == null ? 0 : 1];
if (bp != null)
bp_list [0] = bp.StackId;
var o = JObject.FromObject (new {
callFrames,
reason = "other", //other means breakpoint
hitBreakpoints = bp_list,
});
SendEvent (sessionId, "Debugger.paused", o, token);
return true;
}
async Task OnDefaultContext (SessionId sessionId, ExecutionContext context, CancellationToken token)
{
Log ("verbose", "Default context created, clearing state and sending events");
if (UpdateContext (sessionId, context, out var previousContext)) {
foreach (var kvp in previousContext.BreakpointRequests) {
context.BreakpointRequests[kvp.Key] = kvp.Value.Clone();
}
}
if (await IsRuntimeAlreadyReadyAlready (sessionId, token))
await RuntimeReady (sessionId, token);
}
async Task OnResume (MessageId msd_id, CancellationToken token)
{
//discard managed frames
GetContext (msd_id).ClearState ();
await Task.CompletedTask;
}
async Task<bool> Step (MessageId msg_id, StepKind kind, CancellationToken token)
{
var context = GetContext (msg_id);
if (context.CallStack == null)
return false;
if (context.CallStack.Count <= 1 && kind == StepKind.Out)
return false;
var res = await SendMonoCommand (msg_id, MonoCommands.StartSingleStepping (kind), token);
var ret_code = res.Value? ["result"]? ["value"]?.Value<int> ();
if (ret_code.HasValue && ret_code.Value == 0) {
context.ClearState ();
await SendCommand (msg_id, "Debugger.stepOut", new JObject (), token);
return false;
}
SendResponse (msg_id, Result.Ok (new JObject ()), token);
context.ClearState ();
await SendCommand (msg_id, "Debugger.resume", new JObject (), token);
return true;
}
internal bool TryFindVariableValueInCache(ExecutionContext ctx, string expression, bool only_search_on_this, out JToken obj)
{
if (ctx.LocalsCache.TryGetValue (expression, out obj)) {
if (only_search_on_this && obj["fromThis"] == null)
return false;
return true;
}
return false;
}
internal async Task<JToken> TryGetVariableValue (MessageId msg_id, int scope_id, string expression, bool only_search_on_this, CancellationToken token)
{
JToken thisValue = null;
var context = GetContext (msg_id);
if (context.CallStack == null)
return null;
if (TryFindVariableValueInCache(context, expression, only_search_on_this, out JToken obj))
return obj;
var scope = context.CallStack.FirstOrDefault (s => s.Id == scope_id);
var live_vars = scope.Method.GetLiveVarsAt (scope.Location.CliLocation.Offset);
//get_this
var res = await SendMonoCommand (msg_id, MonoCommands.GetScopeVariables (scope.Id, live_vars.Select (lv => lv.Index).ToArray ()), token);
var scope_values = res.Value? ["result"]? ["value"]?.Values<JObject> ()?.ToArray ();
thisValue = scope_values?.FirstOrDefault (v => v ["name"]?.Value<string> () == "this");
if (!only_search_on_this) {
if (thisValue != null && expression == "this")
return thisValue;
var value = scope_values.SingleOrDefault (sv => sv ["name"]?.Value<string> () == expression);
if (value != null)
return value;
}
//search in scope
if (thisValue != null) {
if (!DotnetObjectId.TryParse (thisValue ["value"] ["objectId"], out var objectId))
return null;
res = await SendMonoCommand (msg_id, MonoCommands.GetDetails (objectId), token);
scope_values = res.Value? ["result"]? ["value"]?.Values<JObject> ().ToArray ();
var foundValue = scope_values.FirstOrDefault (v => v ["name"].Value<string> () == expression);
if (foundValue != null) {
foundValue["fromThis"] = true;
context.LocalsCache[foundValue ["name"].Value<string> ()] = foundValue;
return foundValue;
}
}
return null;
}
async Task<bool> OnEvaluateOnCallFrame (MessageId msg_id, int scope_id, string expression, CancellationToken token)
{
try {
var context = GetContext (msg_id);
if (context.CallStack == null)
return false;
var varValue = await TryGetVariableValue (msg_id, scope_id, expression, false, token);
if (varValue != null) {
SendResponse (msg_id, Result.OkFromObject (new {
result = varValue ["value"]
}), token);
return true;
}
string retValue = await EvaluateExpression.CompileAndRunTheExpression (this, msg_id, scope_id, expression, token);
SendResponse (msg_id, Result.OkFromObject (new {
result = new {
value = retValue
}
}), token);
return true;
} catch (Exception e) {
logger.LogDebug (e, $"Error in EvaluateOnCallFrame for expression '{expression}.");
}
return false;
}
async Task<Result> GetScopeProperties (MessageId msg_id, int scope_id, CancellationToken token)
{
try {
var ctx = GetContext (msg_id);
var scope = ctx.CallStack.FirstOrDefault (s => s.Id == scope_id);
if (scope == null)
return Result.Err (JObject.FromObject (new { message = $"Could not find scope with id #{scope_id}" }));
var vars = scope.Method.GetLiveVarsAt (scope.Location.CliLocation.Offset);
var var_ids = vars.Select (v => v.Index).ToArray ();
var res = await SendMonoCommand (msg_id, MonoCommands.GetScopeVariables (scope.Id, var_ids), token);
//if we fail we just buble that to the IDE (and let it panic over it)
if (res.IsErr)
return res;
var values = res.Value? ["result"]? ["value"]?.Values<JObject> ().ToArray ();
if(values == null)
return Result.OkFromObject (new { result = Array.Empty<object> () });
var var_list = new List<object> ();
int i = 0;
for (; i < vars.Length && i < values.Length; i ++) {
// For async methods, we get locals with names, unlike non-async methods
// and the order may not match the var_ids, so, use the names that they
// come with
if (values [i]["name"] != null)
continue;
ctx.LocalsCache[vars [i].Name] = values [i];
var_list.Add (new { name = vars [i].Name, value = values [i]["value"] });
}
for (; i < values.Length; i ++) {
ctx.LocalsCache[values [i]["name"].ToString()] = values [i];
var_list.Add (values [i]);
}
return Result.OkFromObject (new { result = var_list });
} catch (Exception exception) {
Log ("verbose", $"Error resolving scope properties {exception.Message}");
return Result.Exception (exception);
}
}
async Task<Breakpoint> SetMonoBreakpoint (SessionId sessionId, string reqId, SourceLocation location, CancellationToken token)
{
var bp = new Breakpoint (reqId, location, BreakpointState.Pending);
var asm_name = bp.Location.CliLocation.Method.Assembly.Name;
var method_token = bp.Location.CliLocation.Method.Token;
var il_offset = bp.Location.CliLocation.Offset;
var res = await SendMonoCommand (sessionId, MonoCommands.SetBreakpoint (asm_name, method_token, il_offset), token);
var ret_code = res.Value? ["result"]? ["value"]?.Value<int> ();
if (ret_code.HasValue) {
bp.RemoteId = ret_code.Value;
bp.State = BreakpointState.Active;
//Log ("verbose", $"BP local id {bp.LocalId} enabled with remote id {bp.RemoteId}");
}
return bp;
}
async Task<DebugStore> LoadStore (SessionId sessionId, CancellationToken token)
{
var context = GetContext (sessionId);
if (Interlocked.CompareExchange (ref context.store, new DebugStore (logger), null) != null)
return await context.Source.Task;
try {
var loaded_pdbs = await SendMonoCommand (sessionId, MonoCommands.GetLoadedFiles(), token);
var the_value = loaded_pdbs.Value? ["result"]? ["value"];
var the_pdbs = the_value?.ToObject<string[]> ();
await foreach (var source in context.store.Load(sessionId, the_pdbs, token).WithCancellation (token)) {
var scriptSource = JObject.FromObject (source.ToScriptSource (context.Id, context.AuxData));
Log ("verbose", $"\tsending {source.Url} {context.Id} {sessionId.sessionId}");
SendEvent (sessionId, "Debugger.scriptParsed", scriptSource, token);
foreach (var req in context.BreakpointRequests.Values) {
if (req.TryResolve (source)) {
await SetBreakpoint (sessionId, context.store, req, true, token);
}
}
}
} catch (Exception e) {
context.Source.SetException (e);
}
if (!context.Source.Task.IsCompleted)
context.Source.SetResult (context.store);
return context.store;
}
async Task<DebugStore> RuntimeReady (SessionId sessionId, CancellationToken token)
{
var context = GetContext (sessionId);
if (Interlocked.CompareExchange (ref context.ready, new TaskCompletionSource<DebugStore> (), null) != null)
return await context.ready.Task;
var clear_result = await SendMonoCommand (sessionId, MonoCommands.ClearAllBreakpoints (), token);
if (clear_result.IsErr) {
Log ("verbose", $"Failed to clear breakpoints due to {clear_result}");
}
var store = await LoadStore (sessionId, token);
context.ready.SetResult (store);
SendEvent (sessionId, "Mono.runtimeReady", new JObject (), token);
return store;
}
async Task RemoveBreakpoint(MessageId msg_id, JObject args, CancellationToken token) {
var bpid = args? ["breakpointId"]?.Value<string> ();
var context = GetContext (msg_id);
if (!context.BreakpointRequests.TryGetValue (bpid, out var breakpointRequest))
return;
foreach (var bp in breakpointRequest.Locations) {
var res = await SendMonoCommand (msg_id, MonoCommands.RemoveBreakpoint (bp.RemoteId), token);
var ret_code = res.Value? ["result"]? ["value"]?.Value<int> ();
if (ret_code.HasValue) {
bp.RemoteId = -1;
bp.State = BreakpointState.Disabled;
}
}
breakpointRequest.Locations.Clear ();
}
async Task SetBreakpoint (SessionId sessionId, DebugStore store, BreakpointRequest req, bool sendResolvedEvent, CancellationToken token)
{
var context = GetContext (sessionId);
if (req.Locations.Any ()) {
Log ("debug", $"locations already loaded for {req.Id}");
return;
}
var comparer = new SourceLocation.LocationComparer ();
// if column is specified the frontend wants the exact matches
// and will clear the bp if it isn't close enoug
var locations = store.FindBreakpointLocations (req)
.Distinct (comparer)
.Where (l => l.Line == req.Line && (req.Column == 0 || l.Column == req.Column))
.OrderBy (l => l.Column)
.GroupBy (l => l.Id);
logger.LogDebug ("BP request for '{req}' runtime ready {context.RuntimeReady}", req, GetContext (sessionId).IsRuntimeReady);
var breakpoints = new List<Breakpoint> ();
foreach (var sourceId in locations) {
var loc = sourceId.First ();
var bp = await SetMonoBreakpoint (sessionId, req.Id, loc, token);
// If we didn't successfully enable the breakpoint
// don't add it to the list of locations for this id
if (bp.State != BreakpointState.Active)
continue;
breakpoints.Add (bp);
var resolvedLocation = new {
breakpointId = req.Id,
location = loc.AsLocation ()
};
if (sendResolvedEvent)
SendEvent (sessionId, "Debugger.breakpointResolved", JObject.FromObject (resolvedLocation), token);
}
req.Locations.AddRange (breakpoints);
return;
}
async Task<bool> GetPossibleBreakpoints (MessageId msg, SourceLocation start, SourceLocation end, CancellationToken token)
{
var bps = (await RuntimeReady (msg, token)).FindPossibleBreakpoints (start, end);
if (bps == null)
return false;
var response = new { locations = bps.Select (b => b.AsLocation ()) };
SendResponse (msg, Result.OkFromObject (response), token);
return true;
}
void OnCompileDotnetScript (MessageId msg_id, CancellationToken token)
{
SendResponse (msg_id, Result.OkFromObject (new { }), token);
}
async Task<bool> OnGetScriptSource (MessageId msg_id, string script_id, CancellationToken token)
{
if (!SourceId.TryParse (script_id, out var id))
return false;
var src_file = (await LoadStore (msg_id, token)).GetFileById (id);
try {
var uri = new Uri (src_file.Url);
string source = $"// Unable to find document {src_file.SourceUri}";
using (var data = await src_file.GetSourceAsync (checkHash: false, token: token)) {
if (data.Length == 0)
return false;
using (var reader = new StreamReader (data))
source = await reader.ReadToEndAsync ();
}
SendResponse (msg_id, Result.OkFromObject (new { scriptSource = source }), token);
} catch (Exception e) {
var o = 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.OkFromObject (o), token);
}
return true;
}
async Task DeleteWebDriver (SessionId sessionId, CancellationToken token)
{
// see https://github.com/mono/mono/issues/19549 for background
if (hideWebDriver && sessions.Add (sessionId)) {
var res = await SendCommand (sessionId,
"Page.addScriptToEvaluateOnNewDocument",
JObject.FromObject (new { source = "delete navigator.constructor.prototype.webdriver"}),
token);
if (sessionId != SessionId.Null && !res.IsOk)
sessions.Remove (sessionId);
}
}
}
}

View File

@ -1,71 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.Hosting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
{
public class Program
{
static int Main(string[] args)
{
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
{
Name = "webassembly-debugproxy"
};
app.HelpOption("-?|-h|--help");
var browserHostOption = new CommandOption("-b|--browser-host", CommandOptionType.SingleValue)
{
Description = "Host on which the browser is listening for debug connections. Example: http://localhost:9300"
};
var ownerPidOption = new CommandOption("-op|--owner-pid", CommandOptionType.SingleValue)
{
Description = "ID of the owner process. The debug proxy will shut down if this process exits."
};
app.Options.Add(browserHostOption);
app.Options.Add(ownerPidOption);
app.OnExecute(() =>
{
var browserHost = browserHostOption.HasValue() ? browserHostOption.Value(): "http://127.0.0.1:9222";
var host = DebugProxyHost.CreateDefaultBuilder(args, browserHost).Build();
if (ownerPidOption.HasValue())
{
var ownerProcess = Process.GetProcessById(int.Parse(ownerPidOption.Value()));
ownerProcess.EnableRaisingEvents = true;
ownerProcess.Exited += async (sender, eventArgs) =>
{
Console.WriteLine("Exiting because parent process has exited");
await host.StopAsync();
};
}
host.Run();
return 0;
});
try
{
return app.Execute(args);
}
catch (CommandParsingException cex)
{
app.Error.WriteLine(cex.Message);
app.ShowHelp();
return 1;
}
}
}
}

View File

@ -1,45 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using WebAssembly.Net.Debugging;
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
{
public class Startup
{
public void Configure(IApplicationBuilder app, DebugProxyOptions debugProxyOptions)
{
app.UseDeveloperExceptionPage();
app.UseWebSockets();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
// At the homepage, we check whether we can uniquely identify the target tab
// - If yes, we redirect directly to the debug tools, proxying to that tab
// - If no, we present a list of available tabs for the user to pick from
endpoints.MapGet("/", new TargetPickerUi(debugProxyOptions).Display);
// At this URL, we wire up the actual WebAssembly proxy
endpoints.MapGet("/ws-proxy", async (context) =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return;
}
var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
var browserUri = new Uri(context.Request.Query["browser"]);
var ideSocket = await context.WebSockets.AcceptWebSocketAsync();
await new MonoProxy(loggerFactory).Run(browserUri, ideSocket);
});
});
}
}
}

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Builder
var processStartInfo = new ProcessStartInfo
{
FileName = muxerPath,
Arguments = $"exec \"{executablePath}\" --owner-pid {ownerPid}",
Arguments = $"exec \"{executablePath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
};
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Builder
var debugProxyPath = Path.Combine(
Path.GetDirectoryName(assembly.Location),
"BlazorDebugProxy",
"Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.dll");
"BrowserDebugHost.dll");
if (!File.Exists(debugProxyPath))
{

View File

@ -12,6 +12,9 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.NETCore.BrowserDebugHost.Transport" GeneratePathProperty="true"/>
<!-- Dependencies needed by DebugProxy. -->
<Reference Include= "Newtonsoft.Json" />
</ItemGroup>
<ItemGroup>
@ -22,27 +25,13 @@
<Compile Include="$(ComponentsSharedSourceRoot)\src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
<Compile Include="$(SharedSourceRoot)\CommandLineUtils\Utilities\DotNetMuxer.cs" Link="Shared\DotNetMuxer.cs" />
<!-- Ensure debug proxy is built first, but don't create an actual reference, since we don't want its transitive dependencies. -->
<ProjectReference
Include="..\..\DebugProxy\src\Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj"
ReferenceOutputAssembly="false" />
<Content Include="build\**" Pack="true" PackagePath="build\%(RecursiveDir)%(FileName)%(Extension)" />
</ItemGroup>
<!-- Emit debug proxy binaries to output directory. This lets us launch it as a separate process while keeping the build output self-contained. -->
<Target Name="IncludeDebugProxyBinariesAsContent" BeforeTargets="AssignTargetPaths">
<ItemGroup>
<DebugProxyBinaries Include="$(ArtifactsBinDir)Microsoft.AspNetCore.Components.WebAssembly.DebugProxy\$(Configuration)\$(DefaultNetCoreTargetFramework)\**" />
<!--
For when we're building a package, we use Pack and PackagePath to bundle the debug proxy binaries into 'tools'.
Then we have a custom build target that converts these items into content files in consuming projects. We *don't*
use PackageCopyToOutput because that creates entries that show up in Solution Explorer in consuming projects.
For when we're consuming this from source in this repo, we use Link and CopyToOutputDirectory to produce the
same effect without having the custom build target.
-->
<DebugProxyBinaries Include="$(PkgMicrosoft_NETCore_BrowserDebugHost_Transport)\tools\$(DefaultNetCoreTargetFramework)\**" />
<Content
Include="@(DebugProxyBinaries)"
Pack="true"
@ -50,10 +39,6 @@
Link="BlazorDebugProxy\%(RecursiveDir)%(FileName)%(Extension)"
CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<Error
Text="Debugger proxy files were not found in $(ArtifactsBinDir)Microsoft.AspNetCore.Components.WebAssembly.DebugProxy\$(Configuration)\$(DefaultNetCoreTargetFramework)"
Condition="'@(DebugProxyBinaries->Count())' == '0'" />
</Target>
</Project>

View File

@ -12,7 +12,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
namespace Microsoft.AspNetCore.Components.WebAssembly.Server
{
public class TargetPickerUi
{
@ -23,11 +23,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
IgnoreNullValues = true
};
private readonly DebugProxyOptions _options;
private readonly string BrowserHost = "http://localhost:9222";
private string _debugProxyUrl;
public TargetPickerUi(DebugProxyOptions options)
public TargetPickerUi(string debugProxyUrl)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_debugProxyUrl = debugProxyUrl;
}
public async Task Display(HttpContext context)
@ -37,7 +38,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
var request = context.Request;
var targetApplicationUrl = request.Query["url"];
var debuggerTabsListUrl = $"{_options.BrowserHost}/json";
var debuggerTabsListUrl = $"{BrowserHost}/json";
IEnumerable<BrowserTab> availableTabs;
try
@ -134,17 +135,17 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
private string GetDevToolsUrlWithProxy(HttpRequest request, BrowserTab tabToDebug)
{
var underlyingV8Endpoint = tabToDebug.WebSocketDebuggerUrl;
var proxyEndpoint = GetProxyEndpoint(request, underlyingV8Endpoint);
var devToolsUrlAbsolute = new Uri(_options.BrowserHost + tabToDebug.DevtoolsFrontendUrl);
var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?{proxyEndpoint.Scheme}={proxyEndpoint.Authority}{proxyEndpoint.PathAndQuery}";
var underlyingV8Endpoint = new Uri(tabToDebug.WebSocketDebuggerUrl);
var proxyEndpoint = new Uri(_debugProxyUrl);
var devToolsUrlAbsolute = new Uri(BrowserHost + tabToDebug.DevtoolsFrontendUrl);
var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?{underlyingV8Endpoint.Scheme}={proxyEndpoint.Authority}{underlyingV8Endpoint.PathAndQuery}";
return devToolsUrlWithProxy;
}
private string GetLaunchChromeInstructions(string targetApplicationUrl)
{
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-chrome-debug");
var debuggerPort = new Uri(_options.BrowserHost).Port;
var debuggerPort = new Uri(BrowserHost).Port;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@ -170,7 +171,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
private string GetLaunchEdgeInstructions(string targetApplicationUrl)
{
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-edge-debug");
var debuggerPort = new Uri(_options.BrowserHost).Port;
var debuggerPort = new Uri(BrowserHost).Port;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@ -209,7 +210,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
private async Task<IEnumerable<BrowserTab>> GetOpenedBrowserTabs()
{
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
var jsonResponse = await httpClient.GetStringAsync($"{_options.BrowserHost}/json");
var jsonResponse = await httpClient.GetStringAsync($"{BrowserHost}/json");
return JsonSerializer.Deserialize<BrowserTab[]>(jsonResponse, JsonOptions);
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Net;
using Microsoft.AspNetCore.Components.WebAssembly.Server;
namespace Microsoft.AspNetCore.Builder
{
@ -27,11 +28,12 @@ namespace Microsoft.AspNetCore.Builder
requestPath = "/";
}
// Although we could redirect for every URL we see here, we filter the allowed set
// to ensure this doesn't get misused as some kind of more general redirector
switch (requestPath)
{
case "/":
var targetPickerUi = new TargetPickerUi(debugProxyBaseUrl);
await targetPickerUi.Display(context);
break;
case "/ws-proxy":
context.Response.Redirect($"{debugProxyBaseUrl}{requestPath}{context.Request.QueryString}");
break;