Merge pull request #2047 from dotnet/safia/dbg-proxy

Migrate DebugProxy to dotnet/blazor repo
This commit is contained in:
Safia Abdalla 2020-06-15 10:22:52 -07:00 коммит произвёл GitHub
Родитель cc449601d6 c3f9564774
Коммит 240326739d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 3021 добавлений и 2 удалений

Просмотреть файл

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2010
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B867E038-B3CE-43E3-9292-61568C46CDEB}"
EndProject
@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebAssembly.Runtime", "src\Microsoft.AspNetCore.Components.WebAssembly.Runtime\Microsoft.AspNetCore.Components.WebAssembly.Runtime.csproj", "{E74CC0F5-876C-4DB3-A01F-5D81D5772440}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebAssembly.DebugProxy", "src\Microsoft.AspNetCore.Components.WebAssembly.DebugProxy\Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj", "{695F29C8-65CC-4721-AA03-C225084E6C00}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -23,6 +25,10 @@ Global
{E74CC0F5-876C-4DB3-A01F-5D81D5772440}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E74CC0F5-876C-4DB3-A01F-5D81D5772440}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E74CC0F5-876C-4DB3-A01F-5D81D5772440}.Release|Any CPU.Build.0 = Release|Any CPU
{695F29C8-65CC-4721-AA03-C225084E6C00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{695F29C8-65CC-4721-AA03-C225084E6C00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{695F29C8-65CC-4721-AA03-C225084E6C00}.Release|Any CPU.ActiveCfg = Release|Any CPU
{695F29C8-65CC-4721-AA03-C225084E6C00}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

Просмотреть файл

@ -0,0 +1,4 @@
{
"rollForwardOnNoCandidateFx": 2
}

Просмотреть файл

@ -0,0 +1,10 @@
// 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; }
}
}

Просмотреть файл

@ -0,0 +1,65 @@
// 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;
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<PackageId>Microsoft.AspNetCore.Components.WebAssembly.DebugProxy</PackageId>
<IsShipping>true</IsShipping>
<IsPackable>true</IsPackable>
<OutputType>exe</OutputType>
<HasReferenceAssembly>false</HasReferenceAssembly>
<AssemblyName>DebugProxy</AssemblyName>
<PackageDescription>Debug proxy for use when building Blazor applications.</PackageDescription>
<NuspecFile>Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.nuspec</NuspecFile>
<!-- Set this to false because assemblies should not reference this assembly directly, (except for tests, of course). -->
<IsProjectReferenceProvider>false</IsProjectReferenceProvider>
<NoWarn>$(NoWarn);CS0649</NoWarn>
<LangVersion>8</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
<PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" />
<PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<Target Name="GenerateDotnetTool" BeforeTargets="GenerateNuspec" DependsOnTargets="Publish">
<ItemGroup>
<NuspecProperty Include="Path=..\..\artifacts\bin\Microsoft.AspNetCore.Components.WebAssembly.DebugProxy\$(Configuration)\$(TargetFramework)\" />
</ItemGroup>
</Target>
</Project>

Просмотреть файл

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
$CommonMetadataElements$
</metadata>
<files>
$CommonFileElements$
<file src="..\..\THIRD-PARTY-NOTICES.txt" />
<file src="$Path$*.dll" target="tools" />
<file src="$Path$DebugProxy.dll" target="tools" />
<file src="$Path$DebugProxy.runtimeconfig.json" target="tools" />
<file src="$Path$DebugProxy.deps.json" target="tools" />
</files>
</package>

Просмотреть файл

@ -0,0 +1,843 @@
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 : "";
}
}

Просмотреть файл

@ -0,0 +1,298 @@
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 ();
}
}
}

Просмотреть файл

@ -0,0 +1,337 @@
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;
}
}
}
}

Просмотреть файл

@ -0,0 +1,182 @@
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;
}
}
}

Просмотреть файл

@ -0,0 +1,885 @@
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);
}
}
}
}

Просмотреть файл

@ -0,0 +1,71 @@
// 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;
}
}
}
}

Просмотреть файл

@ -0,0 +1,45 @@
// 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);
});
});
}
}
}

Просмотреть файл

@ -0,0 +1,226 @@
// 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.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
{
public class TargetPickerUi
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
IgnoreNullValues = true
};
private readonly DebugProxyOptions _options;
public TargetPickerUi(DebugProxyOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
}
public async Task Display(HttpContext context)
{
context.Response.ContentType = "text/html";
var request = context.Request;
var targetApplicationUrl = request.Query["url"];
var debuggerTabsListUrl = $"{_options.BrowserHost}/json";
IEnumerable<BrowserTab> availableTabs;
try
{
availableTabs = await GetOpenedBrowserTabs();
}
catch (Exception ex)
{
await context.Response.WriteAsync($@"
<h1>Unable to find debuggable browser tab</h1>
<p>
Could not get a list of browser tabs from <code>{debuggerTabsListUrl}</code>.
Ensure your browser is running with debugging enabled.
</p>
<h2>Resolution</h2>
<p>
<h4>If you are using Google Chrome for your development, follow these instructions:</h4>
{GetLaunchChromeInstructions(targetApplicationUrl)}
</p>
<p>
<h4>If you are using Microsoft Edge (80+) for your development, follow these instructions:</h4>
{GetLaunchEdgeInstructions(targetApplicationUrl)}
</p>
<strong>This should launch a new browser window with debugging enabled..</p>
<h2>Underlying exception:</h2>
<pre>{ex}</pre>
");
return;
}
var matchingTabs = string.IsNullOrEmpty(targetApplicationUrl)
? availableTabs.ToList()
: availableTabs.Where(t => t.Url.Equals(targetApplicationUrl, StringComparison.Ordinal)).ToList();
if (matchingTabs.Count == 1)
{
// We know uniquely which tab to debug, so just redirect
var devToolsUrlWithProxy = GetDevToolsUrlWithProxy(request, matchingTabs.Single());
context.Response.Redirect(devToolsUrlWithProxy);
}
else if (matchingTabs.Count == 0)
{
await context.Response.WriteAsync("<h1>No inspectable pages found</h1>");
var suffix = string.IsNullOrEmpty(targetApplicationUrl)
? string.Empty
: $" matching the URL {WebUtility.HtmlEncode(targetApplicationUrl)}";
await context.Response.WriteAsync($"<p>The list of targets returned by {WebUtility.HtmlEncode(debuggerTabsListUrl)} contains no entries{suffix}.</p>");
await context.Response.WriteAsync("<p>Make sure your browser is displaying the target application.</p>");
}
else
{
await context.Response.WriteAsync("<h1>Inspectable pages</h1>");
await context.Response.WriteAsync(@"
<style type='text/css'>
body {
font-family: Helvetica, Arial, sans-serif;
margin: 2rem 3rem;
}
.inspectable-page {
display: block;
background-color: #eee;
padding: 1rem 1.2rem;
margin-bottom: 1rem;
border-radius: 0.5rem;
text-decoration: none;
color: #888;
}
.inspectable-page:hover {
background-color: #fed;
}
.inspectable-page h3 {
margin-top: 0px;
margin-bottom: 0.3rem;
color: black;
}
</style>
");
foreach (var tab in matchingTabs)
{
var devToolsUrlWithProxy = GetDevToolsUrlWithProxy(request, tab);
await context.Response.WriteAsync(
$"<a class='inspectable-page' href='{WebUtility.HtmlEncode(devToolsUrlWithProxy)}'>"
+ $"<h3>{WebUtility.HtmlEncode(tab.Title)}</h3>{WebUtility.HtmlEncode(tab.Url)}"
+ $"</a>");
}
}
}
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}";
return devToolsUrlWithProxy;
}
private string GetLaunchChromeInstructions(string targetApplicationUrl)
{
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-chrome-debug");
var debuggerPort = new Uri(_options.BrowserHost).Port;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return $@"<p>Press Win+R and enter the following:</p>
<p><strong><code>chrome --remote-debugging-port={debuggerPort} --user-data-dir=""{profilePath}"" {targetApplicationUrl}</code></strong></p>";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return $@"<p>In a terminal window execute the following:</p>
<p><strong><code>google-chrome --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {targetApplicationUrl}</code></strong></p>";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return $@"<p>Execute the following:</p>
<p><strong><code>open /Applications/Google\ Chrome.app --args --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {targetApplicationUrl}</code></strong></p>";
}
else
{
throw new InvalidOperationException("Unknown OS platform");
}
}
private string GetLaunchEdgeInstructions(string targetApplicationUrl)
{
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-edge-debug");
var debuggerPort = new Uri(_options.BrowserHost).Port;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return $@"<p>Press Win+R and enter the following:</p>
<p><strong><code>msedge --remote-debugging-port={debuggerPort} --user-data-dir=""{profilePath}"" --no-first-run {targetApplicationUrl}</code></strong></p>";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return $@"<p>In a terminal window execute the following:</p>
<p><strong><code>open /Applications/Microsoft\ Edge\ Dev.app --args --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {targetApplicationUrl}</code></strong></p>";
}
else
{
return $@"<p>Edge is not current supported on your platform</p>";
}
}
private static Uri GetProxyEndpoint(HttpRequest incomingRequest, string browserEndpoint)
{
var builder = new UriBuilder(
schemeName: incomingRequest.IsHttps ? "wss" : "ws",
hostName: incomingRequest.Host.Host)
{
Path = $"{incomingRequest.PathBase}/ws-proxy",
Query = $"browser={WebUtility.UrlEncode(browserEndpoint)}"
};
if (incomingRequest.Host.Port.HasValue)
{
builder.Port = incomingRequest.Host.Port.Value;
}
return builder.Uri;
}
private async Task<IEnumerable<BrowserTab>> GetOpenedBrowserTabs()
{
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
var jsonResponse = await httpClient.GetStringAsync($"{_options.BrowserHost}/json");
return JsonSerializer.Deserialize<BrowserTab[]>(jsonResponse, JsonOptions);
}
class BrowserTab
{
public string Id { get; set; }
public string Type { get; set; }
public string Url { get; set; }
public string Title { get; set; }
public string DevtoolsFrontendUrl { get; set; }
public string WebSocketDebuggerUrl { get; set; }
}
}
}