Merge pull request #2047 from dotnet/safia/dbg-proxy
Migrate DebugProxy to dotnet/blazor repo
This commit is contained in:
Коммит
240326739d
10
Blazor.sln
10
Blazor.sln
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче