Use MonoProxy package for debugging
This commit is contained in:
parent
432f9b8a91
commit
691910c67d
|
|
@ -124,6 +124,7 @@ and are generated based on the last package release.
|
||||||
<LatestPackageReference Include="Microsoft.Internal.AspNetCore.H2Spec.All" />
|
<LatestPackageReference Include="Microsoft.Internal.AspNetCore.H2Spec.All" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.Internal.Transport" />
|
<LatestPackageReference Include="Microsoft.Extensions.Internal.Transport" />
|
||||||
<LatestPackageReference Include="Microsoft.NETCore.Windows.ApiSets" />
|
<LatestPackageReference Include="Microsoft.NETCore.Windows.ApiSets" />
|
||||||
|
<LatestPackageReference Include="Microsoft.NETCore.BrowserDebugHost.Transport" />
|
||||||
<LatestPackageReference Include="Microsoft.Owin.Security.Cookies" />
|
<LatestPackageReference Include="Microsoft.Owin.Security.Cookies" />
|
||||||
<LatestPackageReference Include="Microsoft.Owin.Testing" />
|
<LatestPackageReference Include="Microsoft.Owin.Testing" />
|
||||||
<LatestPackageReference Include="Microsoft.Web.Administration" />
|
<LatestPackageReference Include="Microsoft.Web.Administration" />
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,10 @@
|
||||||
<Uri>https://github.com/dotnet/runtime</Uri>
|
<Uri>https://github.com/dotnet/runtime</Uri>
|
||||||
<Sha>1dfd9438149f74ae11918a7b0709b8d58c61443f</Sha>
|
<Sha>1dfd9438149f74ae11918a7b0709b8d58c61443f</Sha>
|
||||||
</Dependency>
|
</Dependency>
|
||||||
|
<Dependency Name="Microsoft.NETCore.BrowserDebugHost.Transport" Version="5.0.0-rc.1.20414.5">
|
||||||
|
<Uri>https://github.com/dotnet/runtime</Uri>
|
||||||
|
<Sha>6cc7cffaff2e0413ee84d9a7736f9ae1aa9a3f12</Sha>
|
||||||
|
</Dependency>
|
||||||
</ProductDependencies>
|
</ProductDependencies>
|
||||||
<ToolsetDependencies>
|
<ToolsetDependencies>
|
||||||
<!-- Listed explicitly to workaround https://github.com/dotnet/cli/issues/10528 -->
|
<!-- Listed explicitly to workaround https://github.com/dotnet/cli/issues/10528 -->
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
|
||||||
{
|
|
||||||
public class DebugProxyOptions
|
|
||||||
{
|
|
||||||
public string BrowserHost { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.Hosting
|
|
||||||
{
|
|
||||||
public static class DebugProxyHost
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a custom HostBuilder for the DebugProxyLauncher so that we can inject
|
|
||||||
/// only the needed configurations.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">Command line arguments passed in</param>
|
|
||||||
/// <param name="browserHost">Host where browser is listening for debug connections</param>
|
|
||||||
/// <returns><see cref="IHostBuilder" /></returns>
|
|
||||||
public static IHostBuilder CreateDefaultBuilder(string[] args, string browserHost)
|
|
||||||
{
|
|
||||||
var builder = new HostBuilder();
|
|
||||||
|
|
||||||
builder.ConfigureAppConfiguration((hostingContext, config) =>
|
|
||||||
{
|
|
||||||
if (args != null)
|
|
||||||
{
|
|
||||||
config.AddCommandLine(args);
|
|
||||||
}
|
|
||||||
config.SetBasePath(Directory.GetCurrentDirectory());
|
|
||||||
config.AddJsonFile("blazor-debugproxysettings.json", optional: true, reloadOnChange: true);
|
|
||||||
})
|
|
||||||
.ConfigureLogging((hostingContext, logging) =>
|
|
||||||
{
|
|
||||||
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
|
|
||||||
logging.AddConsole();
|
|
||||||
logging.AddDebug();
|
|
||||||
logging.AddEventSourceLogger();
|
|
||||||
})
|
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
|
||||||
{
|
|
||||||
webBuilder.UseStartup<Startup>();
|
|
||||||
|
|
||||||
// By default we bind to a dyamic port
|
|
||||||
// This can be overridden using an option like "--urls http://localhost:9500"
|
|
||||||
webBuilder.UseUrls("http://127.0.0.1:0");
|
|
||||||
})
|
|
||||||
.ConfigureServices(serviceCollection =>
|
|
||||||
{
|
|
||||||
serviceCollection.AddSingleton(new DebugProxyOptions
|
|
||||||
{
|
|
||||||
BrowserHost = browserHost
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<PackageId>Microsoft.AspNetCore.Components.WebAssembly.DebugProxy</PackageId>
|
|
||||||
<IsShippingPackage>true</IsShippingPackage>
|
|
||||||
<Description>Debug proxy for use when building Blazor applications.</Description>
|
|
||||||
<!-- Set this to false because assemblies should not reference this assembly directly, (except for tests, of course). -->
|
|
||||||
<IsProjectReferenceProvider>false</IsProjectReferenceProvider>
|
|
||||||
<NoWarn>$(NoWarn);CS0649</NoWarn>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="Microsoft.AspNetCore" />
|
|
||||||
<Reference Include="Microsoft.AspNetCore.WebSockets" />
|
|
||||||
<Reference Include="Microsoft.Extensions.Hosting" />
|
|
||||||
|
|
||||||
<!-- Dependencies of ws-proxy sources -->
|
|
||||||
<Reference Include="Newtonsoft.Json" />
|
|
||||||
<Reference Include="Mono.Cecil" />
|
|
||||||
<Reference Include="Microsoft.CodeAnalysis.CSharp" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
|
|
@ -1,843 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Mono.Cecil;
|
|
||||||
using Mono.Cecil.Cil;
|
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using Mono.Cecil.Pdb;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Threading;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
|
|
||||||
namespace WebAssembly.Net.Debugging {
|
|
||||||
internal class BreakpointRequest {
|
|
||||||
public string Id { get; private set; }
|
|
||||||
public string Assembly { get; private set; }
|
|
||||||
public string File { get; private set; }
|
|
||||||
public int Line { get; private set; }
|
|
||||||
public int Column { get; private set; }
|
|
||||||
public MethodInfo Method { get; private set; }
|
|
||||||
|
|
||||||
JObject request;
|
|
||||||
|
|
||||||
public bool IsResolved => Assembly != null;
|
|
||||||
public List<Breakpoint> Locations { get; } = new List<Breakpoint> ();
|
|
||||||
|
|
||||||
public override string ToString ()
|
|
||||||
=> $"BreakpointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}";
|
|
||||||
|
|
||||||
public object AsSetBreakpointByUrlResponse (IEnumerable<object> jsloc)
|
|
||||||
=> new { breakpointId = Id, locations = Locations.Select(l => l.Location.AsLocation ()).Concat (jsloc) };
|
|
||||||
|
|
||||||
public BreakpointRequest () {
|
|
||||||
}
|
|
||||||
|
|
||||||
public BreakpointRequest (string id, MethodInfo method) {
|
|
||||||
Id = id;
|
|
||||||
Method = method;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BreakpointRequest (string id, JObject request) {
|
|
||||||
Id = id;
|
|
||||||
this.request = request;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BreakpointRequest Parse (string id, JObject args)
|
|
||||||
{
|
|
||||||
return new BreakpointRequest (id, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BreakpointRequest Clone ()
|
|
||||||
=> new BreakpointRequest { Id = Id, request = request };
|
|
||||||
|
|
||||||
public bool IsMatch (SourceFile sourceFile)
|
|
||||||
{
|
|
||||||
var url = request? ["url"]?.Value<string> ();
|
|
||||||
if (url == null) {
|
|
||||||
var urlRegex = request?["urlRegex"].Value<string>();
|
|
||||||
var regex = new Regex (urlRegex);
|
|
||||||
return regex.IsMatch (sourceFile.Url.ToString ()) || regex.IsMatch (sourceFile.DocUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sourceFile.Url.ToString () == url || sourceFile.DotNetUrl == url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryResolve (SourceFile sourceFile)
|
|
||||||
{
|
|
||||||
if (!IsMatch (sourceFile))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var line = request? ["lineNumber"]?.Value<int> ();
|
|
||||||
var column = request? ["columnNumber"]?.Value<int> ();
|
|
||||||
|
|
||||||
if (line == null || column == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
Assembly = sourceFile.AssemblyName;
|
|
||||||
File = sourceFile.DebuggerFileName;
|
|
||||||
Line = line.Value;
|
|
||||||
Column = column.Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryResolve (DebugStore store)
|
|
||||||
{
|
|
||||||
if (request == null || store == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return store.AllSources().FirstOrDefault (source => TryResolve (source)) != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class VarInfo {
|
|
||||||
public VarInfo (VariableDebugInformation v)
|
|
||||||
{
|
|
||||||
this.Name = v.Name;
|
|
||||||
this.Index = v.Index;
|
|
||||||
}
|
|
||||||
|
|
||||||
public VarInfo (ParameterDefinition p)
|
|
||||||
{
|
|
||||||
this.Name = p.Name;
|
|
||||||
this.Index = (p.Index + 1) * -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name { get; }
|
|
||||||
public int Index { get; }
|
|
||||||
|
|
||||||
public override string ToString ()
|
|
||||||
=> $"(var-info [{Index}] '{Name}')";
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class CliLocation {
|
|
||||||
public CliLocation (MethodInfo method, int offset)
|
|
||||||
{
|
|
||||||
Method = method;
|
|
||||||
Offset = offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MethodInfo Method { get; }
|
|
||||||
public int Offset { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class SourceLocation {
|
|
||||||
SourceId id;
|
|
||||||
int line;
|
|
||||||
int column;
|
|
||||||
CliLocation cliLoc;
|
|
||||||
|
|
||||||
public SourceLocation (SourceId id, int line, int column)
|
|
||||||
{
|
|
||||||
this.id = id;
|
|
||||||
this.line = line;
|
|
||||||
this.column = column;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SourceLocation (MethodInfo mi, SequencePoint sp)
|
|
||||||
{
|
|
||||||
this.id = mi.SourceId;
|
|
||||||
this.line = sp.StartLine - 1;
|
|
||||||
this.column = sp.StartColumn - 1;
|
|
||||||
this.cliLoc = new CliLocation (mi, sp.Offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SourceId Id { get => id; }
|
|
||||||
public int Line { get => line; }
|
|
||||||
public int Column { get => column; }
|
|
||||||
public CliLocation CliLocation => this.cliLoc;
|
|
||||||
|
|
||||||
public override string ToString ()
|
|
||||||
=> $"{id}:{Line}:{Column}";
|
|
||||||
|
|
||||||
public static SourceLocation Parse (JObject obj)
|
|
||||||
{
|
|
||||||
if (obj == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!SourceId.TryParse (obj ["scriptId"]?.Value<string> (), out var id))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var line = obj ["lineNumber"]?.Value<int> ();
|
|
||||||
var column = obj ["columnNumber"]?.Value<int> ();
|
|
||||||
if (id == null || line == null || column == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new SourceLocation (id, line.Value, column.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal class LocationComparer : EqualityComparer<SourceLocation>
|
|
||||||
{
|
|
||||||
public override bool Equals (SourceLocation l1, SourceLocation l2)
|
|
||||||
{
|
|
||||||
if (l1 == null && l2 == null)
|
|
||||||
return true;
|
|
||||||
else if (l1 == null || l2 == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return (l1.Line == l2.Line &&
|
|
||||||
l1.Column == l2.Column &&
|
|
||||||
l1.Id == l2.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode (SourceLocation loc)
|
|
||||||
{
|
|
||||||
int hCode = loc.Line ^ loc.Column;
|
|
||||||
return loc.Id.GetHashCode () ^ hCode.GetHashCode ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object AsLocation ()
|
|
||||||
=> new {
|
|
||||||
scriptId = id.ToString (),
|
|
||||||
lineNumber = line,
|
|
||||||
columnNumber = column
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class SourceId {
|
|
||||||
const string Scheme = "dotnet://";
|
|
||||||
|
|
||||||
readonly int assembly, document;
|
|
||||||
|
|
||||||
public int Assembly => assembly;
|
|
||||||
public int Document => document;
|
|
||||||
|
|
||||||
internal SourceId (int assembly, int document)
|
|
||||||
{
|
|
||||||
this.assembly = assembly;
|
|
||||||
this.document = document;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SourceId (string id)
|
|
||||||
{
|
|
||||||
if (!TryParse (id, out assembly, out document))
|
|
||||||
throw new ArgumentException ("invalid source identifier", nameof (id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryParse (string id, out SourceId source)
|
|
||||||
{
|
|
||||||
source = null;
|
|
||||||
if (!TryParse (id, out var assembly, out var document))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
source = new SourceId (assembly, document);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool TryParse (string id, out int assembly, out int document)
|
|
||||||
{
|
|
||||||
assembly = document = 0;
|
|
||||||
if (id == null || !id.StartsWith (Scheme, StringComparison.Ordinal))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var sp = id.Substring (Scheme.Length).Split ('_');
|
|
||||||
if (sp.Length != 2)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!int.TryParse (sp [0], out assembly))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!int.TryParse (sp [1], out document))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString ()
|
|
||||||
=> $"{Scheme}{assembly}_{document}";
|
|
||||||
|
|
||||||
public override bool Equals (object obj)
|
|
||||||
{
|
|
||||||
if (obj == null)
|
|
||||||
return false;
|
|
||||||
SourceId that = obj as SourceId;
|
|
||||||
return that.assembly == this.assembly && that.document == this.document;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode ()
|
|
||||||
=> assembly.GetHashCode () ^ document.GetHashCode ();
|
|
||||||
|
|
||||||
public static bool operator == (SourceId a, SourceId b)
|
|
||||||
=> ((object)a == null) ? (object)b == null : a.Equals (b);
|
|
||||||
|
|
||||||
public static bool operator != (SourceId a, SourceId b)
|
|
||||||
=> !a.Equals (b);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class MethodInfo {
|
|
||||||
MethodDefinition methodDef;
|
|
||||||
SourceFile source;
|
|
||||||
|
|
||||||
public SourceId SourceId => source.SourceId;
|
|
||||||
|
|
||||||
public string Name => methodDef.Name;
|
|
||||||
public MethodDebugInformation DebugInformation => methodDef.DebugInformation;
|
|
||||||
|
|
||||||
public SourceLocation StartLocation { get; }
|
|
||||||
public SourceLocation EndLocation { get; }
|
|
||||||
public AssemblyInfo Assembly { get; }
|
|
||||||
public uint Token => methodDef.MetadataToken.RID;
|
|
||||||
|
|
||||||
public MethodInfo (AssemblyInfo assembly, MethodDefinition methodDef, SourceFile source)
|
|
||||||
{
|
|
||||||
this.Assembly = assembly;
|
|
||||||
this.methodDef = methodDef;
|
|
||||||
this.source = source;
|
|
||||||
|
|
||||||
var sps = DebugInformation.SequencePoints;
|
|
||||||
if (sps == null || sps.Count() < 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
SequencePoint start = sps [0];
|
|
||||||
SequencePoint end = sps [0];
|
|
||||||
|
|
||||||
foreach (var sp in sps) {
|
|
||||||
if (sp.StartLine < start.StartLine)
|
|
||||||
start = sp;
|
|
||||||
else if (sp.StartLine == start.StartLine && sp.StartColumn < start.StartColumn)
|
|
||||||
start = sp;
|
|
||||||
|
|
||||||
if (sp.EndLine > end.EndLine)
|
|
||||||
end = sp;
|
|
||||||
else if (sp.EndLine == end.EndLine && sp.EndColumn > end.EndColumn)
|
|
||||||
end = sp;
|
|
||||||
}
|
|
||||||
|
|
||||||
StartLocation = new SourceLocation (this, start);
|
|
||||||
EndLocation = new SourceLocation (this, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SourceLocation GetLocationByIl (int pos)
|
|
||||||
{
|
|
||||||
SequencePoint prev = null;
|
|
||||||
foreach (var sp in DebugInformation.SequencePoints) {
|
|
||||||
if (sp.Offset > pos)
|
|
||||||
break;
|
|
||||||
prev = sp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prev != null)
|
|
||||||
return new SourceLocation (this, prev);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public VarInfo [] GetLiveVarsAt (int offset)
|
|
||||||
{
|
|
||||||
var res = new List<VarInfo> ();
|
|
||||||
|
|
||||||
res.AddRange (methodDef.Parameters.Select (p => new VarInfo (p)));
|
|
||||||
res.AddRange (methodDef.DebugInformation.GetScopes ()
|
|
||||||
.Where (s => s.Start.Offset <= offset && (s.End.IsEndOfMethod || s.End.Offset > offset))
|
|
||||||
.SelectMany (s => s.Variables)
|
|
||||||
.Where (v => !v.IsDebuggerHidden)
|
|
||||||
.Select (v => new VarInfo (v)));
|
|
||||||
|
|
||||||
return res.ToArray ();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString () => "MethodInfo(" + methodDef.FullName + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class TypeInfo {
|
|
||||||
AssemblyInfo assembly;
|
|
||||||
TypeDefinition type;
|
|
||||||
List<MethodInfo> methods;
|
|
||||||
|
|
||||||
public TypeInfo (AssemblyInfo assembly, TypeDefinition type) {
|
|
||||||
this.assembly = assembly;
|
|
||||||
this.type = type;
|
|
||||||
methods = new List<MethodInfo> ();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name => type.Name;
|
|
||||||
public string FullName => type.FullName;
|
|
||||||
public List<MethodInfo> Methods => methods;
|
|
||||||
|
|
||||||
public override string ToString () => "TypeInfo('" + FullName + "')";
|
|
||||||
}
|
|
||||||
|
|
||||||
class AssemblyInfo {
|
|
||||||
static int next_id;
|
|
||||||
ModuleDefinition image;
|
|
||||||
readonly int id;
|
|
||||||
readonly ILogger logger;
|
|
||||||
Dictionary<uint, MethodInfo> methods = new Dictionary<uint, MethodInfo> ();
|
|
||||||
Dictionary<string, string> sourceLinkMappings = new Dictionary<string, string>();
|
|
||||||
Dictionary<string, TypeInfo> typesByName = new Dictionary<string, TypeInfo> ();
|
|
||||||
readonly List<SourceFile> sources = new List<SourceFile>();
|
|
||||||
internal string Url { get; }
|
|
||||||
|
|
||||||
public AssemblyInfo (string url, byte[] assembly, byte[] pdb)
|
|
||||||
{
|
|
||||||
this.id = Interlocked.Increment (ref next_id);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Url = url;
|
|
||||||
ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/);
|
|
||||||
|
|
||||||
// set ReadSymbols = true unconditionally in case there
|
|
||||||
// is an embedded pdb then handle ArgumentException
|
|
||||||
// and assume that if pdb == null that is the cause
|
|
||||||
rp.ReadSymbols = true;
|
|
||||||
rp.SymbolReaderProvider = new PdbReaderProvider ();
|
|
||||||
if (pdb != null)
|
|
||||||
rp.SymbolStream = new MemoryStream (pdb);
|
|
||||||
rp.ReadingMode = ReadingMode.Immediate;
|
|
||||||
rp.InMemory = true;
|
|
||||||
|
|
||||||
this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp);
|
|
||||||
} catch (BadImageFormatException ex) {
|
|
||||||
logger.LogWarning ($"Failed to read assembly as portable PDB: {ex.Message}");
|
|
||||||
} catch (ArgumentException) {
|
|
||||||
// if pdb == null this is expected and we
|
|
||||||
// read the assembly without symbols below
|
|
||||||
if (pdb != null)
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.image == null) {
|
|
||||||
ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/);
|
|
||||||
if (pdb != null) {
|
|
||||||
rp.ReadSymbols = true;
|
|
||||||
rp.SymbolReaderProvider = new PdbReaderProvider ();
|
|
||||||
rp.SymbolStream = new MemoryStream (pdb);
|
|
||||||
}
|
|
||||||
|
|
||||||
rp.ReadingMode = ReadingMode.Immediate;
|
|
||||||
rp.InMemory = true;
|
|
||||||
|
|
||||||
this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp);
|
|
||||||
}
|
|
||||||
|
|
||||||
Populate ();
|
|
||||||
}
|
|
||||||
|
|
||||||
public AssemblyInfo (ILogger logger)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Populate ()
|
|
||||||
{
|
|
||||||
ProcessSourceLink();
|
|
||||||
|
|
||||||
var d2s = new Dictionary<Document, SourceFile> ();
|
|
||||||
|
|
||||||
SourceFile FindSource (Document doc)
|
|
||||||
{
|
|
||||||
if (doc == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (d2s.TryGetValue (doc, out var source))
|
|
||||||
return source;
|
|
||||||
|
|
||||||
var src = new SourceFile (this, sources.Count, doc, GetSourceLinkUrl (doc.Url));
|
|
||||||
sources.Add (src);
|
|
||||||
d2s [doc] = src;
|
|
||||||
return src;
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var type in image.GetTypes()) {
|
|
||||||
var typeInfo = new TypeInfo (this, type);
|
|
||||||
typesByName [type.FullName] = typeInfo;
|
|
||||||
|
|
||||||
foreach (var method in type.Methods) {
|
|
||||||
foreach (var sp in method.DebugInformation.SequencePoints) {
|
|
||||||
var source = FindSource (sp.Document);
|
|
||||||
var methodInfo = new MethodInfo (this, method, source);
|
|
||||||
methods [method.MetadataToken.RID] = methodInfo;
|
|
||||||
if (source != null)
|
|
||||||
source.AddMethod (methodInfo);
|
|
||||||
|
|
||||||
typeInfo.Methods.Add (methodInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProcessSourceLink ()
|
|
||||||
{
|
|
||||||
var sourceLinkDebugInfo = image.CustomDebugInformations.FirstOrDefault (i => i.Kind == CustomDebugInformationKind.SourceLink);
|
|
||||||
|
|
||||||
if (sourceLinkDebugInfo != null) {
|
|
||||||
var sourceLinkContent = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content;
|
|
||||||
|
|
||||||
if (sourceLinkContent != null) {
|
|
||||||
var jObject = JObject.Parse (sourceLinkContent) ["documents"];
|
|
||||||
sourceLinkMappings = JsonConvert.DeserializeObject<Dictionary<string, string>> (jObject.ToString ());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Uri GetSourceLinkUrl (string document)
|
|
||||||
{
|
|
||||||
if (sourceLinkMappings.TryGetValue (document, out string url))
|
|
||||||
return new Uri (url);
|
|
||||||
|
|
||||||
foreach (var sourceLinkDocument in sourceLinkMappings) {
|
|
||||||
string key = sourceLinkDocument.Key;
|
|
||||||
|
|
||||||
if (Path.GetFileName (key) != "*") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyTrim = key.TrimEnd ('*');
|
|
||||||
|
|
||||||
if (document.StartsWith(keyTrim, StringComparison.OrdinalIgnoreCase)) {
|
|
||||||
var docUrlPart = document.Replace (keyTrim, "");
|
|
||||||
return new Uri (sourceLinkDocument.Value.TrimEnd ('*') + docUrlPart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetRelativePath (string relativeTo, string path)
|
|
||||||
{
|
|
||||||
var uri = new Uri (relativeTo, UriKind.RelativeOrAbsolute);
|
|
||||||
var rel = Uri.UnescapeDataString (uri.MakeRelativeUri (new Uri (path, UriKind.RelativeOrAbsolute)).ToString ()).Replace (Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
|
||||||
if (rel.Contains (Path.DirectorySeparatorChar.ToString ()) == false) {
|
|
||||||
rel = $".{ Path.DirectorySeparatorChar }{ rel }";
|
|
||||||
}
|
|
||||||
return rel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<SourceFile> Sources
|
|
||||||
=> this.sources;
|
|
||||||
|
|
||||||
public int Id => id;
|
|
||||||
public string Name => image.Name;
|
|
||||||
|
|
||||||
public SourceFile GetDocById (int document)
|
|
||||||
{
|
|
||||||
return sources.FirstOrDefault (s => s.SourceId.Document == document);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MethodInfo GetMethodByToken (uint token)
|
|
||||||
{
|
|
||||||
methods.TryGetValue (token, out var value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TypeInfo GetTypeByName (string name) {
|
|
||||||
typesByName.TryGetValue (name, out var res);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class SourceFile {
|
|
||||||
Dictionary<uint, MethodInfo> methods;
|
|
||||||
AssemblyInfo assembly;
|
|
||||||
int id;
|
|
||||||
Document doc;
|
|
||||||
|
|
||||||
internal SourceFile (AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri)
|
|
||||||
{
|
|
||||||
this.methods = new Dictionary<uint, MethodInfo> ();
|
|
||||||
this.SourceLinkUri = sourceLinkUri;
|
|
||||||
this.assembly = assembly;
|
|
||||||
this.id = id;
|
|
||||||
this.doc = doc;
|
|
||||||
this.DebuggerFileName = doc.Url.Replace ("\\", "/").Replace (":", "");
|
|
||||||
|
|
||||||
this.SourceUri = new Uri ((Path.IsPathRooted (doc.Url) ? "file://" : "") + doc.Url, UriKind.RelativeOrAbsolute);
|
|
||||||
if (SourceUri.IsFile && File.Exists (SourceUri.LocalPath)) {
|
|
||||||
this.Url = this.SourceUri.ToString ();
|
|
||||||
} else {
|
|
||||||
this.Url = DotNetUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void AddMethod (MethodInfo mi)
|
|
||||||
{
|
|
||||||
if (!this.methods.ContainsKey (mi.Token))
|
|
||||||
this.methods [mi.Token] = mi;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string DebuggerFileName { get; }
|
|
||||||
public string Url { get; }
|
|
||||||
public string AssemblyName => assembly.Name;
|
|
||||||
public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}";
|
|
||||||
|
|
||||||
public SourceId SourceId => new SourceId (assembly.Id, this.id);
|
|
||||||
public Uri SourceLinkUri { get; }
|
|
||||||
public Uri SourceUri { get; }
|
|
||||||
|
|
||||||
public IEnumerable<MethodInfo> Methods => this.methods.Values;
|
|
||||||
|
|
||||||
public string DocUrl => doc.Url;
|
|
||||||
|
|
||||||
public (int startLine, int startColumn, int endLine, int endColumn) GetExtents ()
|
|
||||||
{
|
|
||||||
var start = Methods.OrderBy (m => m.StartLocation.Line).ThenBy (m => m.StartLocation.Column).First ();
|
|
||||||
var end = Methods.OrderByDescending (m => m.EndLocation.Line).ThenByDescending (m => m.EndLocation.Column).First ();
|
|
||||||
return (start.StartLocation.Line, start.StartLocation.Column, end.EndLocation.Line, end.EndLocation.Column);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<MemoryStream> GetDataAsync (Uri uri, CancellationToken token)
|
|
||||||
{
|
|
||||||
var mem = new MemoryStream ();
|
|
||||||
try {
|
|
||||||
if (uri.IsFile && File.Exists (uri.LocalPath)) {
|
|
||||||
using (var file = File.Open (SourceUri.LocalPath, FileMode.Open)) {
|
|
||||||
await file.CopyToAsync (mem, token);
|
|
||||||
mem.Position = 0;
|
|
||||||
}
|
|
||||||
} else if (uri.Scheme == "http" || uri.Scheme == "https") {
|
|
||||||
var client = new HttpClient ();
|
|
||||||
using (var stream = await client.GetStreamAsync (uri)) {
|
|
||||||
await stream.CopyToAsync (mem, token);
|
|
||||||
mem.Position = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return mem;
|
|
||||||
}
|
|
||||||
|
|
||||||
static HashAlgorithm GetHashAlgorithm (DocumentHashAlgorithm algorithm)
|
|
||||||
{
|
|
||||||
switch (algorithm) {
|
|
||||||
case DocumentHashAlgorithm.SHA1: return SHA1.Create ();
|
|
||||||
case DocumentHashAlgorithm.SHA256: return SHA256.Create ();
|
|
||||||
case DocumentHashAlgorithm.MD5: return MD5.Create ();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CheckPdbHash (byte [] computedHash)
|
|
||||||
{
|
|
||||||
if (computedHash.Length != doc.Hash.Length)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (var i = 0; i < computedHash.Length; i++)
|
|
||||||
if (computedHash[i] != doc.Hash[i])
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] ComputePdbHash (Stream sourceStream)
|
|
||||||
{
|
|
||||||
var algorithm = GetHashAlgorithm (doc.HashAlgorithm);
|
|
||||||
if (algorithm != null)
|
|
||||||
using (algorithm)
|
|
||||||
return algorithm.ComputeHash (sourceStream);
|
|
||||||
|
|
||||||
return Array.Empty<byte> ();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Stream> GetSourceAsync (bool checkHash, CancellationToken token = default(CancellationToken))
|
|
||||||
{
|
|
||||||
if (doc.EmbeddedSource.Length > 0)
|
|
||||||
return new MemoryStream (doc.EmbeddedSource, false);
|
|
||||||
|
|
||||||
MemoryStream mem;
|
|
||||||
|
|
||||||
mem = await GetDataAsync (SourceUri, token);
|
|
||||||
if (mem != null && (!checkHash || CheckPdbHash (ComputePdbHash (mem)))) {
|
|
||||||
mem.Position = 0;
|
|
||||||
return mem;
|
|
||||||
}
|
|
||||||
|
|
||||||
mem = await GetDataAsync (SourceLinkUri, token);
|
|
||||||
if (mem != null && (!checkHash || CheckPdbHash (ComputePdbHash (mem)))) {
|
|
||||||
mem.Position = 0;
|
|
||||||
return mem;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MemoryStream.Null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ToScriptSource (int executionContextId, object executionContextAuxData)
|
|
||||||
{
|
|
||||||
return new {
|
|
||||||
scriptId = SourceId.ToString (),
|
|
||||||
url = Url,
|
|
||||||
executionContextId,
|
|
||||||
executionContextAuxData,
|
|
||||||
//hash: should be the v8 hash algo, managed implementation is pending
|
|
||||||
dotNetUrl = DotNetUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DebugStore {
|
|
||||||
List<AssemblyInfo> assemblies = new List<AssemblyInfo> ();
|
|
||||||
readonly HttpClient client;
|
|
||||||
readonly ILogger logger;
|
|
||||||
|
|
||||||
public DebugStore (ILogger logger, HttpClient client) {
|
|
||||||
this.client = client;
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DebugStore (ILogger logger) : this (logger, new HttpClient ())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
class DebugItem {
|
|
||||||
public string Url { get; set; }
|
|
||||||
public Task<byte[][]> Data { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public async IAsyncEnumerable<SourceFile> Load (SessionId sessionId, string [] loaded_files, [EnumeratorCancellation] CancellationToken token)
|
|
||||||
{
|
|
||||||
static bool MatchPdb (string asm, string pdb)
|
|
||||||
=> Path.ChangeExtension (asm, "pdb") == pdb;
|
|
||||||
|
|
||||||
var asm_files = new List<string> ();
|
|
||||||
var pdb_files = new List<string> ();
|
|
||||||
foreach (var file_name in loaded_files) {
|
|
||||||
if (file_name.EndsWith (".pdb", StringComparison.OrdinalIgnoreCase))
|
|
||||||
pdb_files.Add (file_name);
|
|
||||||
else
|
|
||||||
asm_files.Add (file_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DebugItem> steps = new List<DebugItem> ();
|
|
||||||
foreach (var url in asm_files) {
|
|
||||||
try {
|
|
||||||
var pdb = pdb_files.FirstOrDefault (n => MatchPdb (url, n));
|
|
||||||
steps.Add (
|
|
||||||
new DebugItem {
|
|
||||||
Url = url,
|
|
||||||
Data = Task.WhenAll (client.GetByteArrayAsync (url), pdb != null ? client.GetByteArrayAsync (pdb) : Task.FromResult<byte []> (null))
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.LogDebug ($"Failed to read {url} ({e.Message})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var step in steps) {
|
|
||||||
AssemblyInfo assembly = null;
|
|
||||||
try {
|
|
||||||
var bytes = await step.Data;
|
|
||||||
assembly = new AssemblyInfo (step.Url, bytes [0], bytes [1]);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.LogDebug ($"Failed to load {step.Url} ({e.Message})");
|
|
||||||
}
|
|
||||||
if (assembly == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
assemblies.Add (assembly);
|
|
||||||
foreach (var source in assembly.Sources)
|
|
||||||
yield return source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<SourceFile> AllSources ()
|
|
||||||
=> assemblies.SelectMany (a => a.Sources);
|
|
||||||
|
|
||||||
public SourceFile GetFileById (SourceId id)
|
|
||||||
=> AllSources ().SingleOrDefault (f => f.SourceId.Equals (id));
|
|
||||||
|
|
||||||
public AssemblyInfo GetAssemblyByName (string name)
|
|
||||||
=> assemblies.FirstOrDefault (a => a.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
/*
|
|
||||||
V8 uses zero based indexing for both line and column.
|
|
||||||
PPDBs uses one based indexing for both line and column.
|
|
||||||
*/
|
|
||||||
static bool Match (SequencePoint sp, SourceLocation start, SourceLocation end)
|
|
||||||
{
|
|
||||||
var spStart = (Line: sp.StartLine - 1, Column: sp.StartColumn - 1);
|
|
||||||
var spEnd = (Line: sp.EndLine - 1, Column: sp.EndColumn - 1);
|
|
||||||
|
|
||||||
if (start.Line > spEnd.Line)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (start.Column > spEnd.Column && start.Line == spEnd.Line)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (end.Line < spStart.Line)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (end.Column < spStart.Column && end.Line == spStart.Line)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<SourceLocation> FindPossibleBreakpoints (SourceLocation start, SourceLocation end)
|
|
||||||
{
|
|
||||||
//XXX FIXME no idea what todo with locations on different files
|
|
||||||
if (start.Id != end.Id) {
|
|
||||||
logger.LogDebug ($"FindPossibleBreakpoints: documents differ (start: {start.Id}) (end {end.Id}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sourceId = start.Id;
|
|
||||||
|
|
||||||
var doc = GetFileById (sourceId);
|
|
||||||
|
|
||||||
var res = new List<SourceLocation> ();
|
|
||||||
if (doc == null) {
|
|
||||||
logger.LogDebug ($"Could not find document {sourceId}");
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var method in doc.Methods) {
|
|
||||||
foreach (var sequencePoint in method.DebugInformation.SequencePoints) {
|
|
||||||
if (!sequencePoint.IsHidden && Match (sequencePoint, start, end))
|
|
||||||
res.Add (new SourceLocation (method, sequencePoint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
V8 uses zero based indexing for both line and column.
|
|
||||||
PPDBs uses one based indexing for both line and column.
|
|
||||||
*/
|
|
||||||
static bool Match (SequencePoint sp, int line, int column)
|
|
||||||
{
|
|
||||||
var bp = (line: line + 1, column: column + 1);
|
|
||||||
|
|
||||||
if (sp.StartLine > bp.line || sp.EndLine < bp.line)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
//Chrome sends a zero column even if getPossibleBreakpoints say something else
|
|
||||||
if (column == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (sp.StartColumn > bp.column && sp.StartLine == bp.line)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (sp.EndColumn < bp.column && sp.EndLine == bp.line)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<SourceLocation> FindBreakpointLocations (BreakpointRequest request)
|
|
||||||
{
|
|
||||||
request.TryResolve (this);
|
|
||||||
|
|
||||||
var asm = assemblies.FirstOrDefault (a => a.Name.Equals (request.Assembly, StringComparison.OrdinalIgnoreCase));
|
|
||||||
var sourceFile = asm?.Sources?.SingleOrDefault (s => s.DebuggerFileName.Equals (request.File, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (sourceFile == null)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
foreach (var method in sourceFile.Methods) {
|
|
||||||
foreach (var sequencePoint in method.DebugInformation.SequencePoints) {
|
|
||||||
if (!sequencePoint.IsHidden && Match (sequencePoint, request.Line, request.Column))
|
|
||||||
yield return new SourceLocation (method, sequencePoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToUrl (SourceLocation location)
|
|
||||||
=> location != null ? GetFileById (location.Id).Url : "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,298 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
using System.Threading;
|
|
||||||
using System.IO;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace WebAssembly.Net.Debugging {
|
|
||||||
|
|
||||||
internal struct SessionId {
|
|
||||||
public readonly string sessionId;
|
|
||||||
|
|
||||||
public SessionId (string sessionId)
|
|
||||||
{
|
|
||||||
this.sessionId = sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// hashset treats 0 as unset
|
|
||||||
public override int GetHashCode ()
|
|
||||||
=> sessionId?.GetHashCode () ?? -1;
|
|
||||||
|
|
||||||
public override bool Equals (object obj)
|
|
||||||
=> (obj is SessionId) ? ((SessionId) obj).sessionId == sessionId : false;
|
|
||||||
|
|
||||||
public static bool operator == (SessionId a, SessionId b)
|
|
||||||
=> a.sessionId == b.sessionId;
|
|
||||||
|
|
||||||
public static bool operator != (SessionId a, SessionId b)
|
|
||||||
=> a.sessionId != b.sessionId;
|
|
||||||
|
|
||||||
public static SessionId Null { get; } = new SessionId ();
|
|
||||||
|
|
||||||
public override string ToString ()
|
|
||||||
=> $"session-{sessionId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
internal struct MessageId {
|
|
||||||
public readonly string sessionId;
|
|
||||||
public readonly int id;
|
|
||||||
|
|
||||||
public MessageId (string sessionId, int id)
|
|
||||||
{
|
|
||||||
this.sessionId = sessionId;
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator SessionId (MessageId id)
|
|
||||||
=> new SessionId (id.sessionId);
|
|
||||||
|
|
||||||
public override string ToString ()
|
|
||||||
=> $"msg-{sessionId}:::{id}";
|
|
||||||
|
|
||||||
public override int GetHashCode ()
|
|
||||||
=> (sessionId?.GetHashCode () ?? 0) ^ id.GetHashCode ();
|
|
||||||
|
|
||||||
public override bool Equals (object obj)
|
|
||||||
=> (obj is MessageId) ? ((MessageId) obj).sessionId == sessionId && ((MessageId) obj).id == id : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DotnetObjectId {
|
|
||||||
public string Scheme { get; }
|
|
||||||
public string Value { get; }
|
|
||||||
|
|
||||||
public static bool TryParse (JToken jToken, out DotnetObjectId objectId)
|
|
||||||
=> TryParse (jToken?.Value<string>(), out objectId);
|
|
||||||
|
|
||||||
public static bool TryParse (string id, out DotnetObjectId objectId)
|
|
||||||
{
|
|
||||||
objectId = null;
|
|
||||||
if (id == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!id.StartsWith ("dotnet:"))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var parts = id.Split (":", 3);
|
|
||||||
|
|
||||||
if (parts.Length < 3)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
objectId = new DotnetObjectId (parts[1], parts[2]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DotnetObjectId (string scheme, string value)
|
|
||||||
{
|
|
||||||
Scheme = scheme;
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString ()
|
|
||||||
=> $"dotnet:{Scheme}:{Value}";
|
|
||||||
}
|
|
||||||
|
|
||||||
internal struct Result {
|
|
||||||
public JObject Value { get; private set; }
|
|
||||||
public JObject Error { get; private set; }
|
|
||||||
|
|
||||||
public bool IsOk => Value != null;
|
|
||||||
public bool IsErr => Error != null;
|
|
||||||
|
|
||||||
Result (JObject result, JObject error)
|
|
||||||
{
|
|
||||||
if (result != null && error != null)
|
|
||||||
throw new ArgumentException ($"Both {nameof(result)} and {nameof(error)} arguments cannot be non-null.");
|
|
||||||
|
|
||||||
bool resultHasError = String.Compare ((result? ["result"] as JObject)? ["subtype"]?. Value<string> (), "error") == 0;
|
|
||||||
if (result != null && resultHasError) {
|
|
||||||
this.Value = null;
|
|
||||||
this.Error = result;
|
|
||||||
} else {
|
|
||||||
this.Value = result;
|
|
||||||
this.Error = error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Result FromJson (JObject obj)
|
|
||||||
{
|
|
||||||
//Log ("protocol", $"from result: {obj}");
|
|
||||||
return new Result (obj ["result"] as JObject, obj ["error"] as JObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Result Ok (JObject ok)
|
|
||||||
=> new Result (ok, null);
|
|
||||||
|
|
||||||
public static Result OkFromObject (object ok)
|
|
||||||
=> Ok (JObject.FromObject(ok));
|
|
||||||
|
|
||||||
public static Result Err (JObject err)
|
|
||||||
=> new Result (null, err);
|
|
||||||
|
|
||||||
public static Result Err (string msg)
|
|
||||||
=> new Result (null, JObject.FromObject (new { message = msg }));
|
|
||||||
|
|
||||||
public static Result Exception (Exception e)
|
|
||||||
=> new Result (null, JObject.FromObject (new { message = e.Message }));
|
|
||||||
|
|
||||||
public JObject ToJObject (MessageId target) {
|
|
||||||
if (IsOk) {
|
|
||||||
return JObject.FromObject (new {
|
|
||||||
target.id,
|
|
||||||
target.sessionId,
|
|
||||||
result = Value
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return JObject.FromObject (new {
|
|
||||||
target.id,
|
|
||||||
target.sessionId,
|
|
||||||
error = Error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString ()
|
|
||||||
{
|
|
||||||
return $"[Result: IsOk: {IsOk}, IsErr: {IsErr}, Value: {Value?.ToString ()}, Error: {Error?.ToString ()} ]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class MonoCommands {
|
|
||||||
public string expression { get; set; }
|
|
||||||
public string objectGroup { get; set; } = "mono-debugger";
|
|
||||||
public bool includeCommandLineAPI { get; set; } = false;
|
|
||||||
public bool silent { get; set; } = false;
|
|
||||||
public bool returnByValue { get; set; } = true;
|
|
||||||
|
|
||||||
public MonoCommands (string expression)
|
|
||||||
=> this.expression = expression;
|
|
||||||
|
|
||||||
public static MonoCommands GetCallStack ()
|
|
||||||
=> new MonoCommands ("MONO.mono_wasm_get_call_stack()");
|
|
||||||
|
|
||||||
public static MonoCommands IsRuntimeReady ()
|
|
||||||
=> new MonoCommands ("MONO.mono_wasm_runtime_is_ready");
|
|
||||||
|
|
||||||
public static MonoCommands StartSingleStepping (StepKind kind)
|
|
||||||
=> new MonoCommands ($"MONO.mono_wasm_start_single_stepping ({(int)kind})");
|
|
||||||
|
|
||||||
public static MonoCommands GetLoadedFiles ()
|
|
||||||
=> new MonoCommands ("MONO.mono_wasm_get_loaded_files()");
|
|
||||||
|
|
||||||
public static MonoCommands ClearAllBreakpoints ()
|
|
||||||
=> new MonoCommands ("MONO.mono_wasm_clear_all_breakpoints()");
|
|
||||||
|
|
||||||
public static MonoCommands GetDetails (DotnetObjectId objectId, JToken args = null)
|
|
||||||
=> new MonoCommands ($"MONO.mono_wasm_get_details ('{objectId}', {(args ?? "{}")})");
|
|
||||||
|
|
||||||
public static MonoCommands GetScopeVariables (int scopeId, params int[] vars)
|
|
||||||
=> new MonoCommands ($"MONO.mono_wasm_get_variables({scopeId}, [ {string.Join (",", vars)} ])");
|
|
||||||
|
|
||||||
public static MonoCommands SetBreakpoint (string assemblyName, uint methodToken, int ilOffset)
|
|
||||||
=> new MonoCommands ($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})");
|
|
||||||
|
|
||||||
public static MonoCommands RemoveBreakpoint (int breakpointId)
|
|
||||||
=> new MonoCommands ($"MONO.mono_wasm_remove_breakpoint({breakpointId})");
|
|
||||||
|
|
||||||
public static MonoCommands ReleaseObject (DotnetObjectId objectId)
|
|
||||||
=> new MonoCommands ($"MONO.mono_wasm_release_object('{objectId}')");
|
|
||||||
|
|
||||||
public static MonoCommands CallFunctionOn (JToken args)
|
|
||||||
=> new MonoCommands ($"MONO.mono_wasm_call_function_on ({args.ToString ()})");
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum MonoErrorCodes {
|
|
||||||
BpNotFound = 100000,
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class MonoConstants {
|
|
||||||
public const string RUNTIME_IS_READY = "mono_wasm_runtime_ready";
|
|
||||||
}
|
|
||||||
|
|
||||||
class Frame {
|
|
||||||
public Frame (MethodInfo method, SourceLocation location, int id)
|
|
||||||
{
|
|
||||||
this.Method = method;
|
|
||||||
this.Location = location;
|
|
||||||
this.Id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MethodInfo Method { get; private set; }
|
|
||||||
public SourceLocation Location { get; private set; }
|
|
||||||
public int Id { get; private set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class Breakpoint {
|
|
||||||
public SourceLocation Location { get; private set; }
|
|
||||||
public int RemoteId { get; set; }
|
|
||||||
public BreakpointState State { get; set; }
|
|
||||||
public string StackId { get; private set; }
|
|
||||||
|
|
||||||
public static bool TryParseId (string stackId, out int id)
|
|
||||||
{
|
|
||||||
id = -1;
|
|
||||||
if (stackId?.StartsWith ("dotnet:", StringComparison.Ordinal) != true)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return int.TryParse (stackId.Substring ("dotnet:".Length), out id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Breakpoint (string stackId, SourceLocation loc, BreakpointState state)
|
|
||||||
{
|
|
||||||
this.StackId = stackId;
|
|
||||||
this.Location = loc;
|
|
||||||
this.State = state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum BreakpointState {
|
|
||||||
Active,
|
|
||||||
Disabled,
|
|
||||||
Pending
|
|
||||||
}
|
|
||||||
|
|
||||||
enum StepKind {
|
|
||||||
Into,
|
|
||||||
Out,
|
|
||||||
Over
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ExecutionContext {
|
|
||||||
public string DebuggerId { get; set; }
|
|
||||||
public Dictionary<string,BreakpointRequest> BreakpointRequests { get; } = new Dictionary<string,BreakpointRequest> ();
|
|
||||||
|
|
||||||
public TaskCompletionSource<DebugStore> ready = null;
|
|
||||||
public bool IsRuntimeReady => ready != null && ready.Task.IsCompleted;
|
|
||||||
|
|
||||||
public int Id { get; set; }
|
|
||||||
public object AuxData { get; set; }
|
|
||||||
|
|
||||||
public List<Frame> CallStack { get; set; }
|
|
||||||
|
|
||||||
internal DebugStore store;
|
|
||||||
public TaskCompletionSource<DebugStore> Source { get; } = new TaskCompletionSource<DebugStore> ();
|
|
||||||
|
|
||||||
public Dictionary<string, JToken> LocalsCache = new Dictionary<string, JToken> ();
|
|
||||||
|
|
||||||
public DebugStore Store {
|
|
||||||
get {
|
|
||||||
if (store == null || !Source.Task.IsCompleted)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearState ()
|
|
||||||
{
|
|
||||||
CallStack = null;
|
|
||||||
LocalsCache.Clear ();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,337 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Threading;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace WebAssembly.Net.Debugging {
|
|
||||||
|
|
||||||
class DevToolsQueue {
|
|
||||||
Task current_send;
|
|
||||||
List<byte []> pending;
|
|
||||||
|
|
||||||
public WebSocket Ws { get; private set; }
|
|
||||||
public Task CurrentSend { get { return current_send; } }
|
|
||||||
public DevToolsQueue (WebSocket sock)
|
|
||||||
{
|
|
||||||
this.Ws = sock;
|
|
||||||
pending = new List<byte []> ();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Send (byte [] bytes, CancellationToken token)
|
|
||||||
{
|
|
||||||
pending.Add (bytes);
|
|
||||||
if (pending.Count == 1) {
|
|
||||||
if (current_send != null)
|
|
||||||
throw new Exception ("current_send MUST BE NULL IF THERE'S no pending send");
|
|
||||||
//logger.LogTrace ("sending {0} bytes", bytes.Length);
|
|
||||||
current_send = Ws.SendAsync (new ArraySegment<byte> (bytes), WebSocketMessageType.Text, true, token);
|
|
||||||
return current_send;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Pump (CancellationToken token)
|
|
||||||
{
|
|
||||||
current_send = null;
|
|
||||||
pending.RemoveAt (0);
|
|
||||||
|
|
||||||
if (pending.Count > 0) {
|
|
||||||
if (current_send != null)
|
|
||||||
throw new Exception ("current_send MUST BE NULL IF THERE'S no pending send");
|
|
||||||
|
|
||||||
current_send = Ws.SendAsync (new ArraySegment<byte> (pending [0]), WebSocketMessageType.Text, true, token);
|
|
||||||
return current_send;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DevToolsProxy {
|
|
||||||
TaskCompletionSource<bool> side_exception = new TaskCompletionSource<bool> ();
|
|
||||||
TaskCompletionSource<bool> client_initiated_close = new TaskCompletionSource<bool> ();
|
|
||||||
Dictionary<MessageId, TaskCompletionSource<Result>> pending_cmds = new Dictionary<MessageId, TaskCompletionSource<Result>> ();
|
|
||||||
ClientWebSocket browser;
|
|
||||||
WebSocket ide;
|
|
||||||
int next_cmd_id;
|
|
||||||
List<Task> pending_ops = new List<Task> ();
|
|
||||||
List<DevToolsQueue> queues = new List<DevToolsQueue> ();
|
|
||||||
|
|
||||||
protected readonly ILogger logger;
|
|
||||||
|
|
||||||
public DevToolsProxy (ILoggerFactory loggerFactory)
|
|
||||||
{
|
|
||||||
logger = loggerFactory.CreateLogger<DevToolsProxy>();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual Task<bool> AcceptEvent (SessionId sessionId, string method, JObject args, CancellationToken token)
|
|
||||||
{
|
|
||||||
return Task.FromResult (false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual Task<bool> AcceptCommand (MessageId id, string method, JObject args, CancellationToken token)
|
|
||||||
{
|
|
||||||
return Task.FromResult (false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<string> ReadOne (WebSocket socket, CancellationToken token)
|
|
||||||
{
|
|
||||||
byte [] buff = new byte [4000];
|
|
||||||
var mem = new MemoryStream ();
|
|
||||||
while (true) {
|
|
||||||
|
|
||||||
if (socket.State != WebSocketState.Open) {
|
|
||||||
Log ("error", $"DevToolsProxy: Socket is no longer open.");
|
|
||||||
client_initiated_close.TrySetResult (true);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await socket.ReceiveAsync (new ArraySegment<byte> (buff), token);
|
|
||||||
if (result.MessageType == WebSocketMessageType.Close) {
|
|
||||||
client_initiated_close.TrySetResult (true);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
mem.Write (buff, 0, result.Count);
|
|
||||||
|
|
||||||
if (result.EndOfMessage)
|
|
||||||
return Encoding.UTF8.GetString (mem.GetBuffer (), 0, (int)mem.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DevToolsQueue GetQueueForSocket (WebSocket ws)
|
|
||||||
{
|
|
||||||
return queues.FirstOrDefault (q => q.Ws == ws);
|
|
||||||
}
|
|
||||||
|
|
||||||
DevToolsQueue GetQueueForTask (Task task)
|
|
||||||
{
|
|
||||||
return queues.FirstOrDefault (q => q.CurrentSend == task);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Send (WebSocket to, JObject o, CancellationToken token)
|
|
||||||
{
|
|
||||||
var sender = browser == to ? "Send-browser" : "Send-ide";
|
|
||||||
|
|
||||||
var method = o ["method"]?.ToString ();
|
|
||||||
//if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled")
|
|
||||||
Log ("protocol", $"{sender}: " + JsonConvert.SerializeObject (o));
|
|
||||||
var bytes = Encoding.UTF8.GetBytes (o.ToString ());
|
|
||||||
|
|
||||||
var queue = GetQueueForSocket (to);
|
|
||||||
|
|
||||||
var task = queue.Send (bytes, token);
|
|
||||||
if (task != null)
|
|
||||||
pending_ops.Add (task);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task OnEvent (SessionId sessionId, string method, JObject args, CancellationToken token)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (!await AcceptEvent (sessionId, method, args, token)) {
|
|
||||||
//logger.LogDebug ("proxy browser: {0}::{1}",method, args);
|
|
||||||
SendEventInternal (sessionId, method, args, token);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
side_exception.TrySetException (e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task OnCommand (MessageId id, string method, JObject args, CancellationToken token)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (!await AcceptCommand (id, method, args, token)) {
|
|
||||||
var res = await SendCommandInternal (id, method, args, token);
|
|
||||||
SendResponseInternal (id, res, token);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
side_exception.TrySetException (e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnResponse (MessageId id, Result result)
|
|
||||||
{
|
|
||||||
//logger.LogTrace ("got id {0} res {1}", id, result);
|
|
||||||
// Fixme
|
|
||||||
if (pending_cmds.Remove (id, out var task)) {
|
|
||||||
task.SetResult (result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.LogError ("Cannot respond to command: {id} with result: {result} - command is not pending", id, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProcessBrowserMessage (string msg, CancellationToken token)
|
|
||||||
{
|
|
||||||
var res = JObject.Parse (msg);
|
|
||||||
|
|
||||||
var method = res ["method"]?.ToString ();
|
|
||||||
//if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled")
|
|
||||||
Log ("protocol", $"browser: {msg}");
|
|
||||||
|
|
||||||
if (res ["id"] == null)
|
|
||||||
pending_ops.Add (OnEvent (new SessionId (res ["sessionId"]?.Value<string> ()), res ["method"].Value<string> (), res ["params"] as JObject, token));
|
|
||||||
else
|
|
||||||
OnResponse (new MessageId (res ["sessionId"]?.Value<string> (), res ["id"].Value<int> ()), Result.FromJson (res));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProcessIdeMessage (string msg, CancellationToken token)
|
|
||||||
{
|
|
||||||
Log ("protocol", $"ide: {msg}");
|
|
||||||
if (!string.IsNullOrEmpty (msg)) {
|
|
||||||
var res = JObject.Parse (msg);
|
|
||||||
pending_ops.Add (OnCommand (
|
|
||||||
new MessageId (res ["sessionId"]?.Value<string> (), res ["id"].Value<int> ()),
|
|
||||||
res ["method"].Value<string> (),
|
|
||||||
res ["params"] as JObject, token));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task<Result> SendCommand (SessionId id, string method, JObject args, CancellationToken token) {
|
|
||||||
//Log ("verbose", $"sending command {method}: {args}");
|
|
||||||
return await SendCommandInternal (id, method, args, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
Task<Result> SendCommandInternal (SessionId sessionId, string method, JObject args, CancellationToken token)
|
|
||||||
{
|
|
||||||
int id = Interlocked.Increment (ref next_cmd_id);
|
|
||||||
|
|
||||||
var o = JObject.FromObject (new {
|
|
||||||
id,
|
|
||||||
method,
|
|
||||||
@params = args
|
|
||||||
});
|
|
||||||
if (sessionId.sessionId != null)
|
|
||||||
o["sessionId"] = sessionId.sessionId;
|
|
||||||
var tcs = new TaskCompletionSource<Result> ();
|
|
||||||
|
|
||||||
var msgId = new MessageId (sessionId.sessionId, id);
|
|
||||||
//Log ("verbose", $"add cmd id {sessionId}-{id}");
|
|
||||||
pending_cmds[msgId] = tcs;
|
|
||||||
|
|
||||||
Send (this.browser, o, token);
|
|
||||||
return tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendEvent (SessionId sessionId, string method, JObject args, CancellationToken token)
|
|
||||||
{
|
|
||||||
//Log ("verbose", $"sending event {method}: {args}");
|
|
||||||
SendEventInternal (sessionId, method, args, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SendEventInternal (SessionId sessionId, string method, JObject args, CancellationToken token)
|
|
||||||
{
|
|
||||||
var o = JObject.FromObject (new {
|
|
||||||
method,
|
|
||||||
@params = args
|
|
||||||
});
|
|
||||||
if (sessionId.sessionId != null)
|
|
||||||
o["sessionId"] = sessionId.sessionId;
|
|
||||||
|
|
||||||
Send (this.ide, o, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void SendResponse (MessageId id, Result result, CancellationToken token)
|
|
||||||
{
|
|
||||||
SendResponseInternal (id, result, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SendResponseInternal (MessageId id, Result result, CancellationToken token)
|
|
||||||
{
|
|
||||||
JObject o = result.ToJObject (id);
|
|
||||||
if (result.IsErr)
|
|
||||||
logger.LogError ($"sending error response for id: {id} -> {result}");
|
|
||||||
|
|
||||||
Send (this.ide, o, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
// , HttpContext context)
|
|
||||||
public async Task Run (Uri browserUri, WebSocket ideSocket)
|
|
||||||
{
|
|
||||||
Log ("info", $"DevToolsProxy: Starting on {browserUri}");
|
|
||||||
using (this.ide = ideSocket) {
|
|
||||||
Log ("verbose", $"DevToolsProxy: IDE waiting for connection on {browserUri}");
|
|
||||||
queues.Add (new DevToolsQueue (this.ide));
|
|
||||||
using (this.browser = new ClientWebSocket ()) {
|
|
||||||
this.browser.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan;
|
|
||||||
await this.browser.ConnectAsync (browserUri, CancellationToken.None);
|
|
||||||
queues.Add (new DevToolsQueue (this.browser));
|
|
||||||
|
|
||||||
Log ("verbose", $"DevToolsProxy: Client connected on {browserUri}");
|
|
||||||
var x = new CancellationTokenSource ();
|
|
||||||
|
|
||||||
pending_ops.Add (ReadOne (browser, x.Token));
|
|
||||||
pending_ops.Add (ReadOne (ide, x.Token));
|
|
||||||
pending_ops.Add (side_exception.Task);
|
|
||||||
pending_ops.Add (client_initiated_close.Task);
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (!x.IsCancellationRequested) {
|
|
||||||
var task = await Task.WhenAny (pending_ops.ToArray ());
|
|
||||||
//logger.LogTrace ("pump {0} {1}", task, pending_ops.IndexOf (task));
|
|
||||||
if (task == pending_ops [0]) {
|
|
||||||
var msg = ((Task<string>)task).Result;
|
|
||||||
if (msg != null) {
|
|
||||||
pending_ops [0] = ReadOne (browser, x.Token); //queue next read
|
|
||||||
ProcessBrowserMessage (msg, x.Token);
|
|
||||||
}
|
|
||||||
} else if (task == pending_ops [1]) {
|
|
||||||
var msg = ((Task<string>)task).Result;
|
|
||||||
if (msg != null) {
|
|
||||||
pending_ops [1] = ReadOne (ide, x.Token); //queue next read
|
|
||||||
ProcessIdeMessage (msg, x.Token);
|
|
||||||
}
|
|
||||||
} else if (task == pending_ops [2]) {
|
|
||||||
var res = ((Task<bool>)task).Result;
|
|
||||||
throw new Exception ("side task must always complete with an exception, what's going on???");
|
|
||||||
} else if (task == pending_ops [3]) {
|
|
||||||
var res = ((Task<bool>)task).Result;
|
|
||||||
Log ("verbose", $"DevToolsProxy: Client initiated close from {browserUri}");
|
|
||||||
x.Cancel ();
|
|
||||||
} else {
|
|
||||||
//must be a background task
|
|
||||||
pending_ops.Remove (task);
|
|
||||||
var queue = GetQueueForTask (task);
|
|
||||||
if (queue != null) {
|
|
||||||
var tsk = queue.Pump (x.Token);
|
|
||||||
if (tsk != null)
|
|
||||||
pending_ops.Add (tsk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log ("error", $"DevToolsProxy::Run: Exception {e}");
|
|
||||||
//throw;
|
|
||||||
} finally {
|
|
||||||
if (!x.IsCancellationRequested)
|
|
||||||
x.Cancel ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void Log (string priority, string msg)
|
|
||||||
{
|
|
||||||
switch (priority) {
|
|
||||||
case "protocol":
|
|
||||||
logger.LogTrace (msg);
|
|
||||||
break;
|
|
||||||
case "verbose":
|
|
||||||
logger.LogDebug (msg);
|
|
||||||
break;
|
|
||||||
case "info":
|
|
||||||
case "warning":
|
|
||||||
case "error":
|
|
||||||
default:
|
|
||||||
logger.LogDebug (msg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
using System.Threading;
|
|
||||||
using System.IO;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
|
||||||
using Microsoft.CodeAnalysis.Emit;
|
|
||||||
using System.Reflection;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
|
|
||||||
namespace WebAssembly.Net.Debugging {
|
|
||||||
|
|
||||||
internal class EvaluateExpression {
|
|
||||||
|
|
||||||
class FindThisExpression : CSharpSyntaxWalker {
|
|
||||||
public List<string> thisExpressions = new List<string> ();
|
|
||||||
public SyntaxTree syntaxTree;
|
|
||||||
public FindThisExpression (SyntaxTree syntax)
|
|
||||||
{
|
|
||||||
syntaxTree = syntax;
|
|
||||||
}
|
|
||||||
public override void Visit (SyntaxNode node)
|
|
||||||
{
|
|
||||||
if (node is ThisExpressionSyntax) {
|
|
||||||
if (node.Parent is MemberAccessExpressionSyntax thisParent && thisParent.Name is IdentifierNameSyntax) {
|
|
||||||
IdentifierNameSyntax var = thisParent.Name as IdentifierNameSyntax;
|
|
||||||
thisExpressions.Add(var.Identifier.Text);
|
|
||||||
var newRoot = syntaxTree.GetRoot ().ReplaceNode (node.Parent, thisParent.Name);
|
|
||||||
syntaxTree = syntaxTree.WithRootAndOptions (newRoot, syntaxTree.Options);
|
|
||||||
this.Visit (GetExpressionFromSyntaxTree(syntaxTree));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
base.Visit (node);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CheckIfIsProperty (MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token)
|
|
||||||
{
|
|
||||||
foreach (var var in thisExpressions) {
|
|
||||||
JToken value = await proxy.TryGetVariableValue (msg_id, scope_id, var, true, token);
|
|
||||||
if (value == null)
|
|
||||||
throw new Exception ($"The property {var} does not exist in the current context");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FindVariableNMethodCall : CSharpSyntaxWalker {
|
|
||||||
public List<IdentifierNameSyntax> variables = new List<IdentifierNameSyntax> ();
|
|
||||||
public List<ThisExpressionSyntax> thisList = new List<ThisExpressionSyntax> ();
|
|
||||||
public List<InvocationExpressionSyntax> methodCall = new List<InvocationExpressionSyntax> ();
|
|
||||||
public List<object> values = new List<Object> ();
|
|
||||||
|
|
||||||
public override void Visit (SyntaxNode node)
|
|
||||||
{
|
|
||||||
if (node is IdentifierNameSyntax identifier && !variables.Any (x => x.Identifier.Text == identifier.Identifier.Text))
|
|
||||||
variables.Add (identifier);
|
|
||||||
if (node is InvocationExpressionSyntax) {
|
|
||||||
methodCall.Add (node as InvocationExpressionSyntax);
|
|
||||||
throw new Exception ("Method Call is not implemented yet");
|
|
||||||
}
|
|
||||||
if (node is AssignmentExpressionSyntax)
|
|
||||||
throw new Exception ("Assignment is not implemented yet");
|
|
||||||
base.Visit (node);
|
|
||||||
}
|
|
||||||
public async Task<SyntaxTree> ReplaceVars (SyntaxTree syntaxTree, MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token)
|
|
||||||
{
|
|
||||||
CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot ();
|
|
||||||
foreach (var var in variables) {
|
|
||||||
ClassDeclarationSyntax classDeclaration = root.Members.ElementAt (0) as ClassDeclarationSyntax;
|
|
||||||
MethodDeclarationSyntax method = classDeclaration.Members.ElementAt (0) as MethodDeclarationSyntax;
|
|
||||||
|
|
||||||
JToken value = await proxy.TryGetVariableValue (msg_id, scope_id, var.Identifier.Text, false, token);
|
|
||||||
|
|
||||||
if (value == null)
|
|
||||||
throw new Exception ($"The name {var.Identifier.Text} does not exist in the current context");
|
|
||||||
|
|
||||||
values.Add (ConvertJSToCSharpType (value ["value"] ["value"].ToString (), value ["value"] ["type"].ToString ()));
|
|
||||||
|
|
||||||
var updatedMethod = method.AddParameterListParameters (
|
|
||||||
SyntaxFactory.Parameter (
|
|
||||||
SyntaxFactory.Identifier (var.Identifier.Text))
|
|
||||||
.WithType (SyntaxFactory.ParseTypeName (GetTypeFullName(value["value"]["type"].ToString()))));
|
|
||||||
root = root.ReplaceNode (method, updatedMethod);
|
|
||||||
}
|
|
||||||
syntaxTree = syntaxTree.WithRootAndOptions (root, syntaxTree.Options);
|
|
||||||
return syntaxTree;
|
|
||||||
}
|
|
||||||
|
|
||||||
private object ConvertJSToCSharpType (string v, string type)
|
|
||||||
{
|
|
||||||
switch (type) {
|
|
||||||
case "number":
|
|
||||||
return Convert.ChangeType (v, typeof (int));
|
|
||||||
case "string":
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Exception ($"Evaluate of this datatype {type} not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetTypeFullName (string type)
|
|
||||||
{
|
|
||||||
switch (type) {
|
|
||||||
case "number":
|
|
||||||
return typeof (int).FullName;
|
|
||||||
case "string":
|
|
||||||
return typeof (string).FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Exception ($"Evaluate of this datatype {type} not implemented yet");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static SyntaxNode GetExpressionFromSyntaxTree (SyntaxTree syntaxTree)
|
|
||||||
{
|
|
||||||
CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot ();
|
|
||||||
ClassDeclarationSyntax classDeclaration = root.Members.ElementAt (0) as ClassDeclarationSyntax;
|
|
||||||
MethodDeclarationSyntax methodDeclaration = classDeclaration.Members.ElementAt (0) as MethodDeclarationSyntax;
|
|
||||||
BlockSyntax blockValue = methodDeclaration.Body;
|
|
||||||
ReturnStatementSyntax returnValue = blockValue.Statements.ElementAt (0) as ReturnStatementSyntax;
|
|
||||||
InvocationExpressionSyntax expressionInvocation = returnValue.Expression as InvocationExpressionSyntax;
|
|
||||||
MemberAccessExpressionSyntax expressionMember = expressionInvocation.Expression as MemberAccessExpressionSyntax;
|
|
||||||
ParenthesizedExpressionSyntax expressionParenthesized = expressionMember.Expression as ParenthesizedExpressionSyntax;
|
|
||||||
return expressionParenthesized.Expression;
|
|
||||||
}
|
|
||||||
internal static async Task<string> CompileAndRunTheExpression (MonoProxy proxy, MessageId msg_id, int scope_id, string expression, CancellationToken token)
|
|
||||||
{
|
|
||||||
FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall ();
|
|
||||||
string retString;
|
|
||||||
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText (@"
|
|
||||||
using System;
|
|
||||||
public class CompileAndRunTheExpression
|
|
||||||
{
|
|
||||||
public string Evaluate()
|
|
||||||
{
|
|
||||||
return (" + expression + @").ToString();
|
|
||||||
}
|
|
||||||
}");
|
|
||||||
|
|
||||||
FindThisExpression findThisExpression = new FindThisExpression (syntaxTree);
|
|
||||||
var expressionTree = GetExpressionFromSyntaxTree(syntaxTree);
|
|
||||||
findThisExpression.Visit (expressionTree);
|
|
||||||
await findThisExpression.CheckIfIsProperty (proxy, msg_id, scope_id, token);
|
|
||||||
syntaxTree = findThisExpression.syntaxTree;
|
|
||||||
|
|
||||||
expressionTree = GetExpressionFromSyntaxTree (syntaxTree);
|
|
||||||
findVarNMethodCall.Visit (expressionTree);
|
|
||||||
|
|
||||||
syntaxTree = await findVarNMethodCall.ReplaceVars (syntaxTree, proxy, msg_id, scope_id, token);
|
|
||||||
|
|
||||||
MetadataReference [] references = new MetadataReference []
|
|
||||||
{
|
|
||||||
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
|
|
||||||
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)
|
|
||||||
};
|
|
||||||
|
|
||||||
CSharpCompilation compilation = CSharpCompilation.Create (
|
|
||||||
"compileAndRunTheExpression",
|
|
||||||
syntaxTrees: new [] { syntaxTree },
|
|
||||||
references: references,
|
|
||||||
options: new CSharpCompilationOptions (OutputKind.DynamicallyLinkedLibrary));
|
|
||||||
using (var ms = new MemoryStream ()) {
|
|
||||||
EmitResult result = compilation.Emit (ms);
|
|
||||||
ms.Seek (0, SeekOrigin.Begin);
|
|
||||||
Assembly assembly = Assembly.Load (ms.ToArray ());
|
|
||||||
Type type = assembly.GetType ("CompileAndRunTheExpression");
|
|
||||||
object obj = Activator.CreateInstance (type);
|
|
||||||
var ret = type.InvokeMember ("Evaluate",
|
|
||||||
BindingFlags.Default | BindingFlags.InvokeMethod,
|
|
||||||
null,
|
|
||||||
obj,
|
|
||||||
//new object [] { 10 }
|
|
||||||
findVarNMethodCall.values.ToArray ());
|
|
||||||
retString = ret.ToString ();
|
|
||||||
}
|
|
||||||
return retString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,885 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
using System.Threading;
|
|
||||||
using System.IO;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
|
|
||||||
namespace WebAssembly.Net.Debugging {
|
|
||||||
|
|
||||||
internal class MonoProxy : DevToolsProxy {
|
|
||||||
HashSet<SessionId> sessions = new HashSet<SessionId> ();
|
|
||||||
Dictionary<SessionId, ExecutionContext> contexts = new Dictionary<SessionId, ExecutionContext> ();
|
|
||||||
|
|
||||||
public MonoProxy (ILoggerFactory loggerFactory, bool hideWebDriver = true) : base(loggerFactory) { this.hideWebDriver = hideWebDriver; }
|
|
||||||
|
|
||||||
readonly bool hideWebDriver;
|
|
||||||
|
|
||||||
internal ExecutionContext GetContext (SessionId sessionId)
|
|
||||||
{
|
|
||||||
if (contexts.TryGetValue (sessionId, out var context))
|
|
||||||
return context;
|
|
||||||
|
|
||||||
throw new ArgumentException ($"Invalid Session: \"{sessionId}\"", nameof (sessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool UpdateContext (SessionId sessionId, ExecutionContext executionContext, out ExecutionContext previousExecutionContext)
|
|
||||||
{
|
|
||||||
var previous = contexts.TryGetValue (sessionId, out previousExecutionContext);
|
|
||||||
contexts[sessionId] = executionContext;
|
|
||||||
return previous;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal Task<Result> SendMonoCommand (SessionId id, MonoCommands cmd, CancellationToken token)
|
|
||||||
=> SendCommand (id, "Runtime.evaluate", JObject.FromObject (cmd), token);
|
|
||||||
|
|
||||||
protected override async Task<bool> AcceptEvent (SessionId sessionId, string method, JObject args, CancellationToken token)
|
|
||||||
{
|
|
||||||
switch (method) {
|
|
||||||
case "Runtime.consoleAPICalled": {
|
|
||||||
var type = args["type"]?.ToString ();
|
|
||||||
if (type == "debug") {
|
|
||||||
if (args["args"]?[0]?["value"]?.ToString () == MonoConstants.RUNTIME_IS_READY && args["args"]?[1]?["value"]?.ToString () == "fe00e07a-5519-4dfe-b35a-f867dbaf2e28")
|
|
||||||
await RuntimeReady (sessionId, token);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Runtime.executionContextCreated": {
|
|
||||||
SendEvent (sessionId, method, args, token);
|
|
||||||
var ctx = args? ["context"];
|
|
||||||
var aux_data = ctx? ["auxData"] as JObject;
|
|
||||||
var id = ctx ["id"].Value<int> ();
|
|
||||||
if (aux_data != null) {
|
|
||||||
var is_default = aux_data ["isDefault"]?.Value<bool> ();
|
|
||||||
if (is_default == true) {
|
|
||||||
await OnDefaultContext (sessionId, new ExecutionContext { Id = id, AuxData = aux_data }, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.paused": {
|
|
||||||
//TODO figure out how to stich out more frames and, in particular what happens when real wasm is on the stack
|
|
||||||
var top_func = args? ["callFrames"]? [0]? ["functionName"]?.Value<string> ();
|
|
||||||
|
|
||||||
if (top_func == "mono_wasm_fire_bp" || top_func == "_mono_wasm_fire_bp") {
|
|
||||||
return await OnBreakpointHit (sessionId, args, token);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.breakpointResolved": {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.scriptParsed": {
|
|
||||||
var url = args? ["url"]?.Value<string> () ?? "";
|
|
||||||
|
|
||||||
switch (url) {
|
|
||||||
case var _ when url == "":
|
|
||||||
case var _ when url.StartsWith ("wasm://", StringComparison.Ordinal): {
|
|
||||||
Log ("verbose", $"ignoring wasm: Debugger.scriptParsed {url}");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log ("verbose", $"proxying Debugger.scriptParsed ({sessionId.sessionId}) {url} {args}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Target.attachedToTarget": {
|
|
||||||
if (args["targetInfo"]["type"]?.ToString() == "page")
|
|
||||||
await DeleteWebDriver (new SessionId (args["sessionId"]?.ToString ()), token);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<bool> IsRuntimeAlreadyReadyAlready (SessionId sessionId, CancellationToken token)
|
|
||||||
{
|
|
||||||
var res = await SendMonoCommand (sessionId, MonoCommands.IsRuntimeReady (), token);
|
|
||||||
return res.Value? ["result"]? ["value"]?.Value<bool> () ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int bpIdGenerator;
|
|
||||||
|
|
||||||
protected override async Task<bool> AcceptCommand (MessageId id, string method, JObject args, CancellationToken token)
|
|
||||||
{
|
|
||||||
// Inspector doesn't use the Target domain or sessions
|
|
||||||
// so we try to init immediately
|
|
||||||
if (hideWebDriver && id == SessionId.Null)
|
|
||||||
await DeleteWebDriver (id, token);
|
|
||||||
|
|
||||||
if (!contexts.TryGetValue (id, out var context))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
switch (method) {
|
|
||||||
case "Target.attachToTarget": {
|
|
||||||
var resp = await SendCommand (id, method, args, token);
|
|
||||||
await DeleteWebDriver (new SessionId (resp.Value ["sessionId"]?.ToString ()), token);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.enable": {
|
|
||||||
var resp = await SendCommand (id, method, args, token);
|
|
||||||
|
|
||||||
context.DebuggerId = resp.Value ["debuggerId"]?.ToString ();
|
|
||||||
|
|
||||||
if (await IsRuntimeAlreadyReadyAlready (id, token))
|
|
||||||
await RuntimeReady (id, token);
|
|
||||||
|
|
||||||
SendResponse (id,resp,token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.getScriptSource": {
|
|
||||||
var script = args? ["scriptId"]?.Value<string> ();
|
|
||||||
return await OnGetScriptSource (id, script, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Runtime.compileScript": {
|
|
||||||
var exp = args? ["expression"]?.Value<string> ();
|
|
||||||
if (exp.StartsWith ("//dotnet:", StringComparison.Ordinal)) {
|
|
||||||
OnCompileDotnetScript (id, token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.getPossibleBreakpoints": {
|
|
||||||
var resp = await SendCommand (id, method, args, token);
|
|
||||||
if (resp.IsOk && resp.Value["locations"].HasValues) {
|
|
||||||
SendResponse (id, resp, token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var start = SourceLocation.Parse (args? ["start"] as JObject);
|
|
||||||
//FIXME support variant where restrictToFunction=true and end is omitted
|
|
||||||
var end = SourceLocation.Parse (args? ["end"] as JObject);
|
|
||||||
if (start != null && end != null && await GetPossibleBreakpoints (id, start, end, token))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
SendResponse (id, resp, token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.setBreakpoint": {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.setBreakpointByUrl": {
|
|
||||||
var resp = await SendCommand (id, method, args, token);
|
|
||||||
if (!resp.IsOk) {
|
|
||||||
SendResponse (id, resp, token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bpid = resp.Value["breakpointId"]?.ToString ();
|
|
||||||
var locations = resp.Value["locations"]?.Values<object>();
|
|
||||||
var request = BreakpointRequest.Parse (bpid, args);
|
|
||||||
|
|
||||||
// is the store done loading?
|
|
||||||
var loaded = context.Source.Task.IsCompleted;
|
|
||||||
if (!loaded) {
|
|
||||||
// Send and empty response immediately if not
|
|
||||||
// and register the breakpoint for resolution
|
|
||||||
context.BreakpointRequests [bpid] = request;
|
|
||||||
SendResponse (id, resp, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await IsRuntimeAlreadyReadyAlready (id, token)) {
|
|
||||||
var store = await RuntimeReady (id, token);
|
|
||||||
|
|
||||||
Log ("verbose", $"BP req {args}");
|
|
||||||
await SetBreakpoint (id, store, request, !loaded, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loaded) {
|
|
||||||
// we were already loaded so we should send a response
|
|
||||||
// with the locations included and register the request
|
|
||||||
context.BreakpointRequests [bpid] = request;
|
|
||||||
var result = Result.OkFromObject (request.AsSetBreakpointByUrlResponse (locations));
|
|
||||||
SendResponse (id, result, token);
|
|
||||||
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.removeBreakpoint": {
|
|
||||||
await RemoveBreakpoint (id, args, token);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.resume": {
|
|
||||||
await OnResume (id, token);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.stepInto": {
|
|
||||||
return await Step (id, StepKind.Into, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.stepOut": {
|
|
||||||
return await Step (id, StepKind.Out, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.stepOver": {
|
|
||||||
return await Step (id, StepKind.Over, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Debugger.evaluateOnCallFrame": {
|
|
||||||
if (!DotnetObjectId.TryParse (args? ["callFrameId"], out var objectId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
switch (objectId.Scheme) {
|
|
||||||
case "scope":
|
|
||||||
return await OnEvaluateOnCallFrame (id,
|
|
||||||
int.Parse (objectId.Value),
|
|
||||||
args? ["expression"]?.Value<string> (), token);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Runtime.getProperties": {
|
|
||||||
if (!DotnetObjectId.TryParse (args? ["objectId"], out var objectId))
|
|
||||||
break;
|
|
||||||
|
|
||||||
var result = await RuntimeGetProperties (id, objectId, args, token);
|
|
||||||
SendResponse (id, result, token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Runtime.releaseObject": {
|
|
||||||
if (!(DotnetObjectId.TryParse (args ["objectId"], out var objectId) && objectId.Scheme == "cfo_res"))
|
|
||||||
break;
|
|
||||||
|
|
||||||
await SendMonoCommand (id, MonoCommands.ReleaseObject (objectId), token);
|
|
||||||
SendResponse (id, Result.OkFromObject (new{}), token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Protocol extensions
|
|
||||||
case "Dotnet-test.setBreakpointByMethod": {
|
|
||||||
Console.WriteLine ("set-breakpoint-by-method: " + id + " " + args);
|
|
||||||
|
|
||||||
var store = await RuntimeReady (id, token);
|
|
||||||
string aname = args ["assemblyName"]?.Value<string> ();
|
|
||||||
string typeName = args ["typeName"]?.Value<string> ();
|
|
||||||
string methodName = args ["methodName"]?.Value<string> ();
|
|
||||||
if (aname == null || typeName == null || methodName == null) {
|
|
||||||
SendResponse (id, Result.Err ("Invalid protocol message '" + args + "'."), token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAssemblyByName seems to work on file names
|
|
||||||
var assembly = store.GetAssemblyByName (aname);
|
|
||||||
if (assembly == null)
|
|
||||||
assembly = store.GetAssemblyByName (aname + ".exe");
|
|
||||||
if (assembly == null)
|
|
||||||
assembly = store.GetAssemblyByName (aname + ".dll");
|
|
||||||
if (assembly == null) {
|
|
||||||
SendResponse (id, Result.Err ("Assembly '" + aname + "' not found."), token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var type = assembly.GetTypeByName (typeName);
|
|
||||||
if (type == null) {
|
|
||||||
SendResponse (id, Result.Err ($"Type '{typeName}' not found."), token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var methodInfo = type.Methods.FirstOrDefault (m => m.Name == methodName);
|
|
||||||
if (methodInfo == null) {
|
|
||||||
SendResponse (id, Result.Err ($"Method '{typeName}:{methodName}' not found."), token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bpIdGenerator ++;
|
|
||||||
string bpid = "by-method-" + bpIdGenerator.ToString ();
|
|
||||||
var request = new BreakpointRequest (bpid, methodInfo);
|
|
||||||
context.BreakpointRequests[bpid] = request;
|
|
||||||
|
|
||||||
var loc = methodInfo.StartLocation;
|
|
||||||
var bp = await SetMonoBreakpoint (id, bpid, loc, token);
|
|
||||||
if (bp.State != BreakpointState.Active) {
|
|
||||||
// FIXME:
|
|
||||||
throw new NotImplementedException ();
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolvedLocation = new {
|
|
||||||
breakpointId = bpid,
|
|
||||||
location = loc.AsLocation ()
|
|
||||||
};
|
|
||||||
|
|
||||||
SendEvent (id, "Debugger.breakpointResolved", JObject.FromObject (resolvedLocation), token);
|
|
||||||
|
|
||||||
SendResponse (id, Result.OkFromObject (new {
|
|
||||||
result = new { breakpointId = bpid, locations = new object [] { loc.AsLocation () }}
|
|
||||||
}), token);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case "Runtime.callFunctionOn": {
|
|
||||||
if (!DotnetObjectId.TryParse (args ["objectId"], out var objectId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var silent = args ["silent"]?.Value<bool> () ?? false;
|
|
||||||
if (objectId.Scheme == "scope") {
|
|
||||||
var fail = silent ? Result.OkFromObject (new { result = new { } }) : Result.Exception (new ArgumentException ($"Runtime.callFunctionOn not supported with scope ({objectId})."));
|
|
||||||
|
|
||||||
SendResponse (id, fail, token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var returnByValue = args ["returnByValue"]?.Value<bool> () ?? false;
|
|
||||||
var res = await SendMonoCommand (id, MonoCommands.CallFunctionOn (args), token);
|
|
||||||
|
|
||||||
if (!returnByValue &&
|
|
||||||
DotnetObjectId.TryParse (res.Value?["result"]?["value"]?["objectId"], out var resultObjectId) &&
|
|
||||||
resultObjectId.Scheme == "cfo_res")
|
|
||||||
res = Result.OkFromObject (new { result = res.Value ["result"]["value"] });
|
|
||||||
|
|
||||||
if (res.IsErr && silent)
|
|
||||||
res = Result.OkFromObject (new { result = new { } });
|
|
||||||
|
|
||||||
SendResponse (id, res, token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<Result> RuntimeGetProperties (MessageId id, DotnetObjectId objectId, JToken args, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (objectId.Scheme == "scope")
|
|
||||||
return await GetScopeProperties (id, int.Parse (objectId.Value), token);
|
|
||||||
|
|
||||||
var res = await SendMonoCommand (id, MonoCommands.GetDetails (objectId, args), token);
|
|
||||||
if (res.IsErr)
|
|
||||||
return res;
|
|
||||||
|
|
||||||
if (objectId.Scheme == "cfo_res") {
|
|
||||||
// Runtime.callFunctionOn result object
|
|
||||||
var value_json_str = res.Value ["result"]?["value"]?["__value_as_json_string__"]?.Value<string> ();
|
|
||||||
if (value_json_str != null) {
|
|
||||||
res = Result.OkFromObject (new {
|
|
||||||
result = JArray.Parse (value_json_str.Replace (@"\""", "\""))
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res = Result.OkFromObject (new { result = new {} });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res = Result.Ok (JObject.FromObject (new { result = res.Value ["result"] ["value"] }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
//static int frame_id=0;
|
|
||||||
async Task<bool> OnBreakpointHit (SessionId sessionId, JObject args, CancellationToken token)
|
|
||||||
{
|
|
||||||
//FIXME we should send release objects every now and then? Or intercept those we inject and deal in the runtime
|
|
||||||
var res = await SendMonoCommand (sessionId, MonoCommands.GetCallStack(), token);
|
|
||||||
var orig_callframes = args? ["callFrames"]?.Values<JObject> ();
|
|
||||||
var context = GetContext (sessionId);
|
|
||||||
|
|
||||||
if (res.IsErr) {
|
|
||||||
//Give up and send the original call stack
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//step one, figure out where did we hit
|
|
||||||
var res_value = res.Value? ["result"]? ["value"];
|
|
||||||
if (res_value == null || res_value is JValue) {
|
|
||||||
//Give up and send the original call stack
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log ("verbose", $"call stack (err is {res.Error} value is:\n{res.Value}");
|
|
||||||
var bp_id = res_value? ["breakpoint_id"]?.Value<int> ();
|
|
||||||
Log ("verbose", $"We just hit bp {bp_id}");
|
|
||||||
if (!bp_id.HasValue) {
|
|
||||||
//Give up and send the original call stack
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bp = context.BreakpointRequests.Values.SelectMany (v => v.Locations).FirstOrDefault (b => b.RemoteId == bp_id.Value);
|
|
||||||
|
|
||||||
var callFrames = new List<object> ();
|
|
||||||
foreach (var frame in orig_callframes) {
|
|
||||||
var function_name = frame ["functionName"]?.Value<string> ();
|
|
||||||
var url = frame ["url"]?.Value<string> ();
|
|
||||||
if ("mono_wasm_fire_bp" == function_name || "_mono_wasm_fire_bp" == function_name) {
|
|
||||||
var frames = new List<Frame> ();
|
|
||||||
int frame_id = 0;
|
|
||||||
var the_mono_frames = res.Value? ["result"]? ["value"]? ["frames"]?.Values<JObject> ();
|
|
||||||
|
|
||||||
foreach (var mono_frame in the_mono_frames) {
|
|
||||||
++frame_id;
|
|
||||||
var il_pos = mono_frame ["il_pos"].Value<int> ();
|
|
||||||
var method_token = mono_frame ["method_token"].Value<uint> ();
|
|
||||||
var assembly_name = mono_frame ["assembly_name"].Value<string> ();
|
|
||||||
|
|
||||||
// This can be different than `method.Name`, like in case of generic methods
|
|
||||||
var method_name = mono_frame ["method_name"]?.Value<string> ();
|
|
||||||
|
|
||||||
var store = await LoadStore (sessionId, token);
|
|
||||||
var asm = store.GetAssemblyByName (assembly_name);
|
|
||||||
if (asm == null) {
|
|
||||||
Log ("info",$"Unable to find assembly: {assembly_name}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var method = asm.GetMethodByToken (method_token);
|
|
||||||
|
|
||||||
if (method == null) {
|
|
||||||
Log ("info", $"Unable to find il offset: {il_pos} in method token: {method_token} assembly name: {assembly_name}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var location = method?.GetLocationByIl (il_pos);
|
|
||||||
|
|
||||||
// When hitting a breakpoint on the "IncrementCount" method in the standard
|
|
||||||
// Blazor project template, one of the stack frames is inside mscorlib.dll
|
|
||||||
// and we get location==null for it. It will trigger a NullReferenceException
|
|
||||||
// if we don't skip over that stack frame.
|
|
||||||
if (location == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log ("info", $"frame il offset: {il_pos} method token: {method_token} assembly name: {assembly_name}");
|
|
||||||
Log ("info", $"\tmethod {method_name} location: {location}");
|
|
||||||
frames.Add (new Frame (method, location, frame_id-1));
|
|
||||||
|
|
||||||
callFrames.Add (new {
|
|
||||||
functionName = method_name,
|
|
||||||
callFrameId = $"dotnet:scope:{frame_id-1}",
|
|
||||||
functionLocation = method.StartLocation.AsLocation (),
|
|
||||||
|
|
||||||
location = location.AsLocation (),
|
|
||||||
|
|
||||||
url = store.ToUrl (location),
|
|
||||||
|
|
||||||
scopeChain = new [] {
|
|
||||||
new {
|
|
||||||
type = "local",
|
|
||||||
@object = new {
|
|
||||||
@type = "object",
|
|
||||||
className = "Object",
|
|
||||||
description = "Object",
|
|
||||||
objectId = $"dotnet:scope:{frame_id-1}",
|
|
||||||
},
|
|
||||||
name = method_name,
|
|
||||||
startLocation = method.StartLocation.AsLocation (),
|
|
||||||
endLocation = method.EndLocation.AsLocation (),
|
|
||||||
}}
|
|
||||||
});
|
|
||||||
|
|
||||||
context.CallStack = frames;
|
|
||||||
|
|
||||||
}
|
|
||||||
} else if (!(function_name.StartsWith ("wasm-function", StringComparison.Ordinal)
|
|
||||||
|| url.StartsWith ("wasm://wasm/", StringComparison.Ordinal))) {
|
|
||||||
callFrames.Add (frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var bp_list = new string [bp == null ? 0 : 1];
|
|
||||||
if (bp != null)
|
|
||||||
bp_list [0] = bp.StackId;
|
|
||||||
|
|
||||||
var o = JObject.FromObject (new {
|
|
||||||
callFrames,
|
|
||||||
reason = "other", //other means breakpoint
|
|
||||||
hitBreakpoints = bp_list,
|
|
||||||
});
|
|
||||||
|
|
||||||
SendEvent (sessionId, "Debugger.paused", o, token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task OnDefaultContext (SessionId sessionId, ExecutionContext context, CancellationToken token)
|
|
||||||
{
|
|
||||||
Log ("verbose", "Default context created, clearing state and sending events");
|
|
||||||
if (UpdateContext (sessionId, context, out var previousContext)) {
|
|
||||||
foreach (var kvp in previousContext.BreakpointRequests) {
|
|
||||||
context.BreakpointRequests[kvp.Key] = kvp.Value.Clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await IsRuntimeAlreadyReadyAlready (sessionId, token))
|
|
||||||
await RuntimeReady (sessionId, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task OnResume (MessageId msd_id, CancellationToken token)
|
|
||||||
{
|
|
||||||
//discard managed frames
|
|
||||||
GetContext (msd_id).ClearState ();
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<bool> Step (MessageId msg_id, StepKind kind, CancellationToken token)
|
|
||||||
{
|
|
||||||
var context = GetContext (msg_id);
|
|
||||||
if (context.CallStack == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (context.CallStack.Count <= 1 && kind == StepKind.Out)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var res = await SendMonoCommand (msg_id, MonoCommands.StartSingleStepping (kind), token);
|
|
||||||
|
|
||||||
var ret_code = res.Value? ["result"]? ["value"]?.Value<int> ();
|
|
||||||
|
|
||||||
if (ret_code.HasValue && ret_code.Value == 0) {
|
|
||||||
context.ClearState ();
|
|
||||||
await SendCommand (msg_id, "Debugger.stepOut", new JObject (), token);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SendResponse (msg_id, Result.Ok (new JObject ()), token);
|
|
||||||
|
|
||||||
context.ClearState ();
|
|
||||||
|
|
||||||
await SendCommand (msg_id, "Debugger.resume", new JObject (), token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool TryFindVariableValueInCache(ExecutionContext ctx, string expression, bool only_search_on_this, out JToken obj)
|
|
||||||
{
|
|
||||||
if (ctx.LocalsCache.TryGetValue (expression, out obj)) {
|
|
||||||
if (only_search_on_this && obj["fromThis"] == null)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task<JToken> TryGetVariableValue (MessageId msg_id, int scope_id, string expression, bool only_search_on_this, CancellationToken token)
|
|
||||||
{
|
|
||||||
JToken thisValue = null;
|
|
||||||
var context = GetContext (msg_id);
|
|
||||||
if (context.CallStack == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (TryFindVariableValueInCache(context, expression, only_search_on_this, out JToken obj))
|
|
||||||
return obj;
|
|
||||||
|
|
||||||
var scope = context.CallStack.FirstOrDefault (s => s.Id == scope_id);
|
|
||||||
var live_vars = scope.Method.GetLiveVarsAt (scope.Location.CliLocation.Offset);
|
|
||||||
//get_this
|
|
||||||
var res = await SendMonoCommand (msg_id, MonoCommands.GetScopeVariables (scope.Id, live_vars.Select (lv => lv.Index).ToArray ()), token);
|
|
||||||
|
|
||||||
var scope_values = res.Value? ["result"]? ["value"]?.Values<JObject> ()?.ToArray ();
|
|
||||||
thisValue = scope_values?.FirstOrDefault (v => v ["name"]?.Value<string> () == "this");
|
|
||||||
|
|
||||||
if (!only_search_on_this) {
|
|
||||||
if (thisValue != null && expression == "this")
|
|
||||||
return thisValue;
|
|
||||||
|
|
||||||
var value = scope_values.SingleOrDefault (sv => sv ["name"]?.Value<string> () == expression);
|
|
||||||
if (value != null)
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
//search in scope
|
|
||||||
if (thisValue != null) {
|
|
||||||
if (!DotnetObjectId.TryParse (thisValue ["value"] ["objectId"], out var objectId))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
res = await SendMonoCommand (msg_id, MonoCommands.GetDetails (objectId), token);
|
|
||||||
scope_values = res.Value? ["result"]? ["value"]?.Values<JObject> ().ToArray ();
|
|
||||||
var foundValue = scope_values.FirstOrDefault (v => v ["name"].Value<string> () == expression);
|
|
||||||
if (foundValue != null) {
|
|
||||||
foundValue["fromThis"] = true;
|
|
||||||
context.LocalsCache[foundValue ["name"].Value<string> ()] = foundValue;
|
|
||||||
return foundValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<bool> OnEvaluateOnCallFrame (MessageId msg_id, int scope_id, string expression, CancellationToken token)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
var context = GetContext (msg_id);
|
|
||||||
if (context.CallStack == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var varValue = await TryGetVariableValue (msg_id, scope_id, expression, false, token);
|
|
||||||
|
|
||||||
if (varValue != null) {
|
|
||||||
SendResponse (msg_id, Result.OkFromObject (new {
|
|
||||||
result = varValue ["value"]
|
|
||||||
}), token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
string retValue = await EvaluateExpression.CompileAndRunTheExpression (this, msg_id, scope_id, expression, token);
|
|
||||||
SendResponse (msg_id, Result.OkFromObject (new {
|
|
||||||
result = new {
|
|
||||||
value = retValue
|
|
||||||
}
|
|
||||||
}), token);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.LogDebug (e, $"Error in EvaluateOnCallFrame for expression '{expression}.");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<Result> GetScopeProperties (MessageId msg_id, int scope_id, CancellationToken token)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
var ctx = GetContext (msg_id);
|
|
||||||
var scope = ctx.CallStack.FirstOrDefault (s => s.Id == scope_id);
|
|
||||||
if (scope == null)
|
|
||||||
return Result.Err (JObject.FromObject (new { message = $"Could not find scope with id #{scope_id}" }));
|
|
||||||
|
|
||||||
var vars = scope.Method.GetLiveVarsAt (scope.Location.CliLocation.Offset);
|
|
||||||
|
|
||||||
var var_ids = vars.Select (v => v.Index).ToArray ();
|
|
||||||
var res = await SendMonoCommand (msg_id, MonoCommands.GetScopeVariables (scope.Id, var_ids), token);
|
|
||||||
|
|
||||||
//if we fail we just buble that to the IDE (and let it panic over it)
|
|
||||||
if (res.IsErr)
|
|
||||||
return res;
|
|
||||||
|
|
||||||
var values = res.Value? ["result"]? ["value"]?.Values<JObject> ().ToArray ();
|
|
||||||
|
|
||||||
if(values == null)
|
|
||||||
return Result.OkFromObject (new { result = Array.Empty<object> () });
|
|
||||||
|
|
||||||
var var_list = new List<object> ();
|
|
||||||
int i = 0;
|
|
||||||
for (; i < vars.Length && i < values.Length; i ++) {
|
|
||||||
// For async methods, we get locals with names, unlike non-async methods
|
|
||||||
// and the order may not match the var_ids, so, use the names that they
|
|
||||||
// come with
|
|
||||||
if (values [i]["name"] != null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ctx.LocalsCache[vars [i].Name] = values [i];
|
|
||||||
var_list.Add (new { name = vars [i].Name, value = values [i]["value"] });
|
|
||||||
}
|
|
||||||
for (; i < values.Length; i ++) {
|
|
||||||
ctx.LocalsCache[values [i]["name"].ToString()] = values [i];
|
|
||||||
var_list.Add (values [i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.OkFromObject (new { result = var_list });
|
|
||||||
} catch (Exception exception) {
|
|
||||||
Log ("verbose", $"Error resolving scope properties {exception.Message}");
|
|
||||||
return Result.Exception (exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<Breakpoint> SetMonoBreakpoint (SessionId sessionId, string reqId, SourceLocation location, CancellationToken token)
|
|
||||||
{
|
|
||||||
var bp = new Breakpoint (reqId, location, BreakpointState.Pending);
|
|
||||||
var asm_name = bp.Location.CliLocation.Method.Assembly.Name;
|
|
||||||
var method_token = bp.Location.CliLocation.Method.Token;
|
|
||||||
var il_offset = bp.Location.CliLocation.Offset;
|
|
||||||
|
|
||||||
var res = await SendMonoCommand (sessionId, MonoCommands.SetBreakpoint (asm_name, method_token, il_offset), token);
|
|
||||||
var ret_code = res.Value? ["result"]? ["value"]?.Value<int> ();
|
|
||||||
|
|
||||||
if (ret_code.HasValue) {
|
|
||||||
bp.RemoteId = ret_code.Value;
|
|
||||||
bp.State = BreakpointState.Active;
|
|
||||||
//Log ("verbose", $"BP local id {bp.LocalId} enabled with remote id {bp.RemoteId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return bp;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<DebugStore> LoadStore (SessionId sessionId, CancellationToken token)
|
|
||||||
{
|
|
||||||
var context = GetContext (sessionId);
|
|
||||||
|
|
||||||
if (Interlocked.CompareExchange (ref context.store, new DebugStore (logger), null) != null)
|
|
||||||
return await context.Source.Task;
|
|
||||||
|
|
||||||
try {
|
|
||||||
var loaded_pdbs = await SendMonoCommand (sessionId, MonoCommands.GetLoadedFiles(), token);
|
|
||||||
var the_value = loaded_pdbs.Value? ["result"]? ["value"];
|
|
||||||
var the_pdbs = the_value?.ToObject<string[]> ();
|
|
||||||
|
|
||||||
await foreach (var source in context.store.Load(sessionId, the_pdbs, token).WithCancellation (token)) {
|
|
||||||
var scriptSource = JObject.FromObject (source.ToScriptSource (context.Id, context.AuxData));
|
|
||||||
Log ("verbose", $"\tsending {source.Url} {context.Id} {sessionId.sessionId}");
|
|
||||||
|
|
||||||
SendEvent (sessionId, "Debugger.scriptParsed", scriptSource, token);
|
|
||||||
|
|
||||||
foreach (var req in context.BreakpointRequests.Values) {
|
|
||||||
if (req.TryResolve (source)) {
|
|
||||||
await SetBreakpoint (sessionId, context.store, req, true, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
context.Source.SetException (e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!context.Source.Task.IsCompleted)
|
|
||||||
context.Source.SetResult (context.store);
|
|
||||||
return context.store;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<DebugStore> RuntimeReady (SessionId sessionId, CancellationToken token)
|
|
||||||
{
|
|
||||||
var context = GetContext (sessionId);
|
|
||||||
if (Interlocked.CompareExchange (ref context.ready, new TaskCompletionSource<DebugStore> (), null) != null)
|
|
||||||
return await context.ready.Task;
|
|
||||||
|
|
||||||
var clear_result = await SendMonoCommand (sessionId, MonoCommands.ClearAllBreakpoints (), token);
|
|
||||||
if (clear_result.IsErr) {
|
|
||||||
Log ("verbose", $"Failed to clear breakpoints due to {clear_result}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var store = await LoadStore (sessionId, token);
|
|
||||||
|
|
||||||
context.ready.SetResult (store);
|
|
||||||
SendEvent (sessionId, "Mono.runtimeReady", new JObject (), token);
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RemoveBreakpoint(MessageId msg_id, JObject args, CancellationToken token) {
|
|
||||||
var bpid = args? ["breakpointId"]?.Value<string> ();
|
|
||||||
|
|
||||||
var context = GetContext (msg_id);
|
|
||||||
if (!context.BreakpointRequests.TryGetValue (bpid, out var breakpointRequest))
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var bp in breakpointRequest.Locations) {
|
|
||||||
var res = await SendMonoCommand (msg_id, MonoCommands.RemoveBreakpoint (bp.RemoteId), token);
|
|
||||||
var ret_code = res.Value? ["result"]? ["value"]?.Value<int> ();
|
|
||||||
|
|
||||||
if (ret_code.HasValue) {
|
|
||||||
bp.RemoteId = -1;
|
|
||||||
bp.State = BreakpointState.Disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
breakpointRequest.Locations.Clear ();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task SetBreakpoint (SessionId sessionId, DebugStore store, BreakpointRequest req, bool sendResolvedEvent, CancellationToken token)
|
|
||||||
{
|
|
||||||
var context = GetContext (sessionId);
|
|
||||||
if (req.Locations.Any ()) {
|
|
||||||
Log ("debug", $"locations already loaded for {req.Id}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var comparer = new SourceLocation.LocationComparer ();
|
|
||||||
// if column is specified the frontend wants the exact matches
|
|
||||||
// and will clear the bp if it isn't close enoug
|
|
||||||
var locations = store.FindBreakpointLocations (req)
|
|
||||||
.Distinct (comparer)
|
|
||||||
.Where (l => l.Line == req.Line && (req.Column == 0 || l.Column == req.Column))
|
|
||||||
.OrderBy (l => l.Column)
|
|
||||||
.GroupBy (l => l.Id);
|
|
||||||
|
|
||||||
logger.LogDebug ("BP request for '{req}' runtime ready {context.RuntimeReady}", req, GetContext (sessionId).IsRuntimeReady);
|
|
||||||
|
|
||||||
var breakpoints = new List<Breakpoint> ();
|
|
||||||
|
|
||||||
foreach (var sourceId in locations) {
|
|
||||||
var loc = sourceId.First ();
|
|
||||||
var bp = await SetMonoBreakpoint (sessionId, req.Id, loc, token);
|
|
||||||
|
|
||||||
// If we didn't successfully enable the breakpoint
|
|
||||||
// don't add it to the list of locations for this id
|
|
||||||
if (bp.State != BreakpointState.Active)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
breakpoints.Add (bp);
|
|
||||||
|
|
||||||
var resolvedLocation = new {
|
|
||||||
breakpointId = req.Id,
|
|
||||||
location = loc.AsLocation ()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sendResolvedEvent)
|
|
||||||
SendEvent (sessionId, "Debugger.breakpointResolved", JObject.FromObject (resolvedLocation), token);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Locations.AddRange (breakpoints);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<bool> GetPossibleBreakpoints (MessageId msg, SourceLocation start, SourceLocation end, CancellationToken token)
|
|
||||||
{
|
|
||||||
var bps = (await RuntimeReady (msg, token)).FindPossibleBreakpoints (start, end);
|
|
||||||
|
|
||||||
if (bps == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var response = new { locations = bps.Select (b => b.AsLocation ()) };
|
|
||||||
|
|
||||||
SendResponse (msg, Result.OkFromObject (response), token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnCompileDotnetScript (MessageId msg_id, CancellationToken token)
|
|
||||||
{
|
|
||||||
SendResponse (msg_id, Result.OkFromObject (new { }), token);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<bool> OnGetScriptSource (MessageId msg_id, string script_id, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (!SourceId.TryParse (script_id, out var id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var src_file = (await LoadStore (msg_id, token)).GetFileById (id);
|
|
||||||
|
|
||||||
try {
|
|
||||||
var uri = new Uri (src_file.Url);
|
|
||||||
string source = $"// Unable to find document {src_file.SourceUri}";
|
|
||||||
|
|
||||||
using (var data = await src_file.GetSourceAsync (checkHash: false, token: token)) {
|
|
||||||
if (data.Length == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
using (var reader = new StreamReader (data))
|
|
||||||
source = await reader.ReadToEndAsync ();
|
|
||||||
}
|
|
||||||
SendResponse (msg_id, Result.OkFromObject (new { scriptSource = source }), token);
|
|
||||||
} catch (Exception e) {
|
|
||||||
var o = new {
|
|
||||||
scriptSource = $"// Unable to read document ({e.Message})\n" +
|
|
||||||
$"Local path: {src_file?.SourceUri}\n" +
|
|
||||||
$"SourceLink path: {src_file?.SourceLinkUri}\n"
|
|
||||||
};
|
|
||||||
|
|
||||||
SendResponse (msg_id, Result.OkFromObject (o), token);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task DeleteWebDriver (SessionId sessionId, CancellationToken token)
|
|
||||||
{
|
|
||||||
// see https://github.com/mono/mono/issues/19549 for background
|
|
||||||
if (hideWebDriver && sessions.Add (sessionId)) {
|
|
||||||
var res = await SendCommand (sessionId,
|
|
||||||
"Page.addScriptToEvaluateOnNewDocument",
|
|
||||||
JObject.FromObject (new { source = "delete navigator.constructor.prototype.webdriver"}),
|
|
||||||
token);
|
|
||||||
|
|
||||||
if (sessionId != SessionId.Null && !res.IsOk)
|
|
||||||
sessions.Remove (sessionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
|
||||||
{
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
static int Main(string[] args)
|
|
||||||
{
|
|
||||||
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
|
|
||||||
{
|
|
||||||
Name = "webassembly-debugproxy"
|
|
||||||
};
|
|
||||||
app.HelpOption("-?|-h|--help");
|
|
||||||
|
|
||||||
var browserHostOption = new CommandOption("-b|--browser-host", CommandOptionType.SingleValue)
|
|
||||||
{
|
|
||||||
Description = "Host on which the browser is listening for debug connections. Example: http://localhost:9300"
|
|
||||||
};
|
|
||||||
|
|
||||||
var ownerPidOption = new CommandOption("-op|--owner-pid", CommandOptionType.SingleValue)
|
|
||||||
{
|
|
||||||
Description = "ID of the owner process. The debug proxy will shut down if this process exits."
|
|
||||||
};
|
|
||||||
|
|
||||||
app.Options.Add(browserHostOption);
|
|
||||||
app.Options.Add(ownerPidOption);
|
|
||||||
|
|
||||||
app.OnExecute(() =>
|
|
||||||
{
|
|
||||||
var browserHost = browserHostOption.HasValue() ? browserHostOption.Value(): "http://127.0.0.1:9222";
|
|
||||||
var host = DebugProxyHost.CreateDefaultBuilder(args, browserHost).Build();
|
|
||||||
|
|
||||||
if (ownerPidOption.HasValue())
|
|
||||||
{
|
|
||||||
var ownerProcess = Process.GetProcessById(int.Parse(ownerPidOption.Value()));
|
|
||||||
ownerProcess.EnableRaisingEvents = true;
|
|
||||||
ownerProcess.Exited += async (sender, eventArgs) =>
|
|
||||||
{
|
|
||||||
Console.WriteLine("Exiting because parent process has exited");
|
|
||||||
await host.StopAsync();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
host.Run();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return app.Execute(args);
|
|
||||||
}
|
|
||||||
catch (CommandParsingException cex)
|
|
||||||
{
|
|
||||||
app.Error.WriteLine(cex.Message);
|
|
||||||
app.ShowHelp();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using WebAssembly.Net.Debugging;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
|
||||||
{
|
|
||||||
public class Startup
|
|
||||||
{
|
|
||||||
public void Configure(IApplicationBuilder app, DebugProxyOptions debugProxyOptions)
|
|
||||||
{
|
|
||||||
app.UseDeveloperExceptionPage();
|
|
||||||
app.UseWebSockets();
|
|
||||||
app.UseRouting();
|
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
|
||||||
{
|
|
||||||
// At the homepage, we check whether we can uniquely identify the target tab
|
|
||||||
// - If yes, we redirect directly to the debug tools, proxying to that tab
|
|
||||||
// - If no, we present a list of available tabs for the user to pick from
|
|
||||||
endpoints.MapGet("/", new TargetPickerUi(debugProxyOptions).Display);
|
|
||||||
|
|
||||||
// At this URL, we wire up the actual WebAssembly proxy
|
|
||||||
endpoints.MapGet("/ws-proxy", async (context) =>
|
|
||||||
{
|
|
||||||
if (!context.WebSockets.IsWebSocketRequest)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
|
|
||||||
var browserUri = new Uri(context.Request.Query["browser"]);
|
|
||||||
var ideSocket = await context.WebSockets.AcceptWebSocketAsync();
|
|
||||||
await new MonoProxy(loggerFactory).Run(browserUri, ideSocket);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
var processStartInfo = new ProcessStartInfo
|
var processStartInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = muxerPath,
|
FileName = muxerPath,
|
||||||
Arguments = $"exec \"{executablePath}\" --owner-pid {ownerPid}",
|
Arguments = $"exec \"{executablePath}\"",
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
};
|
};
|
||||||
|
|
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
var debugProxyPath = Path.Combine(
|
var debugProxyPath = Path.Combine(
|
||||||
Path.GetDirectoryName(assembly.Location),
|
Path.GetDirectoryName(assembly.Location),
|
||||||
"BlazorDebugProxy",
|
"BlazorDebugProxy",
|
||||||
"Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.dll");
|
"BrowserDebugHost.dll");
|
||||||
|
|
||||||
if (!File.Exists(debugProxyPath))
|
if (!File.Exists(debugProxyPath))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
|
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
|
||||||
|
<Reference Include="Microsoft.NETCore.BrowserDebugHost.Transport" GeneratePathProperty="true"/>
|
||||||
|
<!-- Dependencies needed by DebugProxy. -->
|
||||||
|
<Reference Include= "Newtonsoft.Json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -22,27 +25,13 @@
|
||||||
<Compile Include="$(ComponentsSharedSourceRoot)\src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
|
<Compile Include="$(ComponentsSharedSourceRoot)\src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
|
||||||
<Compile Include="$(SharedSourceRoot)\CommandLineUtils\Utilities\DotNetMuxer.cs" Link="Shared\DotNetMuxer.cs" />
|
<Compile Include="$(SharedSourceRoot)\CommandLineUtils\Utilities\DotNetMuxer.cs" Link="Shared\DotNetMuxer.cs" />
|
||||||
|
|
||||||
<!-- Ensure debug proxy is built first, but don't create an actual reference, since we don't want its transitive dependencies. -->
|
|
||||||
<ProjectReference
|
|
||||||
Include="..\..\DebugProxy\src\Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj"
|
|
||||||
ReferenceOutputAssembly="false" />
|
|
||||||
|
|
||||||
<Content Include="build\**" Pack="true" PackagePath="build\%(RecursiveDir)%(FileName)%(Extension)" />
|
<Content Include="build\**" Pack="true" PackagePath="build\%(RecursiveDir)%(FileName)%(Extension)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Emit debug proxy binaries to output directory. This lets us launch it as a separate process while keeping the build output self-contained. -->
|
<!-- Emit debug proxy binaries to output directory. This lets us launch it as a separate process while keeping the build output self-contained. -->
|
||||||
<Target Name="IncludeDebugProxyBinariesAsContent" BeforeTargets="AssignTargetPaths">
|
<Target Name="IncludeDebugProxyBinariesAsContent" BeforeTargets="AssignTargetPaths">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<DebugProxyBinaries Include="$(ArtifactsBinDir)Microsoft.AspNetCore.Components.WebAssembly.DebugProxy\$(Configuration)\$(DefaultNetCoreTargetFramework)\**" />
|
<DebugProxyBinaries Include="$(PkgMicrosoft_NETCore_BrowserDebugHost_Transport)\tools\$(DefaultNetCoreTargetFramework)\**" />
|
||||||
|
|
||||||
<!--
|
|
||||||
For when we're building a package, we use Pack and PackagePath to bundle the debug proxy binaries into 'tools'.
|
|
||||||
Then we have a custom build target that converts these items into content files in consuming projects. We *don't*
|
|
||||||
use PackageCopyToOutput because that creates entries that show up in Solution Explorer in consuming projects.
|
|
||||||
|
|
||||||
For when we're consuming this from source in this repo, we use Link and CopyToOutputDirectory to produce the
|
|
||||||
same effect without having the custom build target.
|
|
||||||
-->
|
|
||||||
<Content
|
<Content
|
||||||
Include="@(DebugProxyBinaries)"
|
Include="@(DebugProxyBinaries)"
|
||||||
Pack="true"
|
Pack="true"
|
||||||
|
|
@ -50,10 +39,6 @@
|
||||||
Link="BlazorDebugProxy\%(RecursiveDir)%(FileName)%(Extension)"
|
Link="BlazorDebugProxy\%(RecursiveDir)%(FileName)%(Extension)"
|
||||||
CopyToOutputDirectory="PreserveNewest" />
|
CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Error
|
|
||||||
Text="Debugger proxy files were not found in $(ArtifactsBinDir)Microsoft.AspNetCore.Components.WebAssembly.DebugProxy\$(Configuration)\$(DefaultNetCoreTargetFramework)"
|
|
||||||
Condition="'@(DebugProxyBinaries->Count())' == '0'" />
|
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Server
|
||||||
{
|
{
|
||||||
public class TargetPickerUi
|
public class TargetPickerUi
|
||||||
{
|
{
|
||||||
|
|
@ -23,11 +23,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
||||||
IgnoreNullValues = true
|
IgnoreNullValues = true
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly DebugProxyOptions _options;
|
private readonly string BrowserHost = "http://localhost:9222";
|
||||||
|
private string _debugProxyUrl;
|
||||||
|
|
||||||
public TargetPickerUi(DebugProxyOptions options)
|
public TargetPickerUi(string debugProxyUrl)
|
||||||
{
|
{
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_debugProxyUrl = debugProxyUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Display(HttpContext context)
|
public async Task Display(HttpContext context)
|
||||||
|
|
@ -37,7 +38,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
||||||
var request = context.Request;
|
var request = context.Request;
|
||||||
var targetApplicationUrl = request.Query["url"];
|
var targetApplicationUrl = request.Query["url"];
|
||||||
|
|
||||||
var debuggerTabsListUrl = $"{_options.BrowserHost}/json";
|
var debuggerTabsListUrl = $"{BrowserHost}/json";
|
||||||
IEnumerable<BrowserTab> availableTabs;
|
IEnumerable<BrowserTab> availableTabs;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -134,17 +135,17 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
||||||
|
|
||||||
private string GetDevToolsUrlWithProxy(HttpRequest request, BrowserTab tabToDebug)
|
private string GetDevToolsUrlWithProxy(HttpRequest request, BrowserTab tabToDebug)
|
||||||
{
|
{
|
||||||
var underlyingV8Endpoint = tabToDebug.WebSocketDebuggerUrl;
|
var underlyingV8Endpoint = new Uri(tabToDebug.WebSocketDebuggerUrl);
|
||||||
var proxyEndpoint = GetProxyEndpoint(request, underlyingV8Endpoint);
|
var proxyEndpoint = new Uri(_debugProxyUrl);
|
||||||
var devToolsUrlAbsolute = new Uri(_options.BrowserHost + tabToDebug.DevtoolsFrontendUrl);
|
var devToolsUrlAbsolute = new Uri(BrowserHost + tabToDebug.DevtoolsFrontendUrl);
|
||||||
var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?{proxyEndpoint.Scheme}={proxyEndpoint.Authority}{proxyEndpoint.PathAndQuery}";
|
var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?{underlyingV8Endpoint.Scheme}={proxyEndpoint.Authority}{underlyingV8Endpoint.PathAndQuery}";
|
||||||
return devToolsUrlWithProxy;
|
return devToolsUrlWithProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetLaunchChromeInstructions(string targetApplicationUrl)
|
private string GetLaunchChromeInstructions(string targetApplicationUrl)
|
||||||
{
|
{
|
||||||
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-chrome-debug");
|
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-chrome-debug");
|
||||||
var debuggerPort = new Uri(_options.BrowserHost).Port;
|
var debuggerPort = new Uri(BrowserHost).Port;
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
|
|
@ -170,7 +171,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
||||||
private string GetLaunchEdgeInstructions(string targetApplicationUrl)
|
private string GetLaunchEdgeInstructions(string targetApplicationUrl)
|
||||||
{
|
{
|
||||||
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-edge-debug");
|
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-edge-debug");
|
||||||
var debuggerPort = new Uri(_options.BrowserHost).Port;
|
var debuggerPort = new Uri(BrowserHost).Port;
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
|
|
@ -209,7 +210,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
||||||
private async Task<IEnumerable<BrowserTab>> GetOpenedBrowserTabs()
|
private async Task<IEnumerable<BrowserTab>> GetOpenedBrowserTabs()
|
||||||
{
|
{
|
||||||
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
|
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
|
||||||
var jsonResponse = await httpClient.GetStringAsync($"{_options.BrowserHost}/json");
|
var jsonResponse = await httpClient.GetStringAsync($"{BrowserHost}/json");
|
||||||
return JsonSerializer.Deserialize<BrowserTab[]>(jsonResponse, JsonOptions);
|
return JsonSerializer.Deserialize<BrowserTab[]>(jsonResponse, JsonOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Server;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Builder
|
namespace Microsoft.AspNetCore.Builder
|
||||||
{
|
{
|
||||||
|
|
@ -27,11 +28,12 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
requestPath = "/";
|
requestPath = "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Although we could redirect for every URL we see here, we filter the allowed set
|
|
||||||
// to ensure this doesn't get misused as some kind of more general redirector
|
|
||||||
switch (requestPath)
|
switch (requestPath)
|
||||||
{
|
{
|
||||||
case "/":
|
case "/":
|
||||||
|
var targetPickerUi = new TargetPickerUi(debugProxyBaseUrl);
|
||||||
|
await targetPickerUi.Display(context);
|
||||||
|
break;
|
||||||
case "/ws-proxy":
|
case "/ws-proxy":
|
||||||
context.Response.Redirect($"{debugProxyBaseUrl}{requestPath}{context.Request.QueryString}");
|
context.Response.Redirect($"{debugProxyBaseUrl}{requestPath}{context.Request.QueryString}");
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue