Include Mono Debug proxy code in Server project (temporarily until it's shipped as a NuGet package)
This commit is contained in:
parent
f483e6293e
commit
c881a63a78
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
|
|
@ -8,6 +8,9 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCorePackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="$(AspNetCorePackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="$(AspNetCorePackageVersion)" />
|
||||
|
||||
<!-- Used by ws-proxy sources only. Remove this once we're able to consume ws-proxy as a NuGet package. -->
|
||||
<PackageReference Include="Mono.Cecil" Version="0.10.0-beta7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
@echo off
|
||||
echo |----
|
||||
echo | Copying the ws-proxy sources here is a temporary step until ws-proxy is
|
||||
echo | distributed as a NuGet package.
|
||||
echo | ...
|
||||
echo | Instead of dealing with Git submodules, this script simply fetches the
|
||||
echo | latest sources so they can be built directly inside this project (hence
|
||||
echo | we don't have to publish our own separate package for this).
|
||||
echo | ...
|
||||
echo | When updating, you'll need to re-apply any patches we've made manually.
|
||||
echo |----
|
||||
@echo on
|
||||
|
||||
cd /D "%~dp0"
|
||||
rmdir /s /q ws-proxy
|
||||
git clone https://github.com/kumpera/ws-proxy.git
|
||||
rmdir /s /q ws-proxy\.git
|
||||
del ws-proxy\*.csproj
|
||||
del ws-proxy\*.sln
|
||||
del ws-proxy\Program.cs
|
||||
|
|
@ -0,0 +1,519 @@
|
|||
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;
|
||||
|
||||
namespace WsProxy {
|
||||
internal class BreakPointRequest {
|
||||
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 static BreakPointRequest Parse (JObject args)
|
||||
{
|
||||
if (args == null)
|
||||
return null;
|
||||
|
||||
var url = args? ["url"]?.Value<string> ();
|
||||
if (!url.StartsWith ("dotnet://", StringComparison.InvariantCulture))
|
||||
return null;
|
||||
|
||||
var parts = url.Substring ("dotnet://".Length).Split ('/');
|
||||
if (parts.Length != 2)
|
||||
return null;
|
||||
|
||||
var line = args? ["lineNumber"]?.Value<int> ();
|
||||
var column = args? ["columnNumber"]?.Value<int> ();
|
||||
if (line == null || column == null)
|
||||
return null;
|
||||
|
||||
return new BreakPointRequest () {
|
||||
Assembly = parts [0],
|
||||
File = parts [1],
|
||||
Line = line.Value,
|
||||
Column = column.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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; private set; }
|
||||
public int Index { get; private set; }
|
||||
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return $"(var-info [{Index}] '{Name}')";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class CliLocation {
|
||||
|
||||
private MethodInfo method;
|
||||
private int offset;
|
||||
|
||||
public CliLocation (MethodInfo method, int offset)
|
||||
{
|
||||
this.method = method;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public MethodInfo Method { get => method; }
|
||||
public int Offset { get => offset; }
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
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 ()
|
||||
{
|
||||
return $"{id}:{Line}:{Column}";
|
||||
}
|
||||
|
||||
public static SourceLocation Parse (JObject obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return null;
|
||||
|
||||
var id = SourceId.TryParse (obj ["scriptId"]?.Value<string> ());
|
||||
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 JObject ToJObject ()
|
||||
{
|
||||
return JObject.FromObject (new {
|
||||
scriptId = id.ToString (),
|
||||
lineNumber = line,
|
||||
columnNumber = column
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class SourceId {
|
||||
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)
|
||||
{
|
||||
id = id.Substring ("dotnet://".Length);
|
||||
var sp = id.Split ('_');
|
||||
this.assembly = int.Parse (sp [0]);
|
||||
this.document = int.Parse (sp [1]);
|
||||
}
|
||||
|
||||
public static SourceId TryParse (string id)
|
||||
{
|
||||
if (!id.StartsWith ("dotnet://", StringComparison.InvariantCulture))
|
||||
return null;
|
||||
return new SourceId (id);
|
||||
|
||||
}
|
||||
public override string ToString ()
|
||||
{
|
||||
return $"dotnet://{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 ()
|
||||
{
|
||||
return this.assembly.GetHashCode () ^ this.document.GetHashCode ();
|
||||
}
|
||||
|
||||
public static bool operator == (SourceId a, SourceId b)
|
||||
{
|
||||
if ((object)a == null)
|
||||
return (object)b == null;
|
||||
return a.Equals (b);
|
||||
}
|
||||
|
||||
public static bool operator != (SourceId a, SourceId b)
|
||||
{
|
||||
return !a.Equals (b);
|
||||
}
|
||||
}
|
||||
|
||||
internal class MethodInfo {
|
||||
AssemblyInfo assembly;
|
||||
internal MethodDefinition methodDef;
|
||||
SourceFile source;
|
||||
|
||||
public SourceId SourceId => source.SourceId;
|
||||
|
||||
public string Name => methodDef.Name;
|
||||
|
||||
public SourceLocation StartLocation { get; private set; }
|
||||
public SourceLocation EndLocation { get; private set; }
|
||||
public AssemblyInfo Assembly => assembly;
|
||||
public int Token => (int)methodDef.MetadataToken.RID;
|
||||
|
||||
public MethodInfo (AssemblyInfo assembly, MethodDefinition methodDef, SourceFile source)
|
||||
{
|
||||
this.assembly = assembly;
|
||||
this.methodDef = methodDef;
|
||||
this.source = source;
|
||||
|
||||
var sps = methodDef.DebugInformation.SequencePoints;
|
||||
if (sps != null && sps.Count > 0) {
|
||||
StartLocation = new SourceLocation (this, sps [0]);
|
||||
EndLocation = new SourceLocation (this, sps [sps.Count - 1]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public SourceLocation GetLocationByIl (int pos)
|
||||
{
|
||||
SequencePoint prev = null;
|
||||
foreach (var sp in methodDef.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 ();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class AssemblyInfo {
|
||||
static int next_id;
|
||||
ModuleDefinition image;
|
||||
readonly int id;
|
||||
Dictionary<int, MethodInfo> methods = new Dictionary<int, MethodInfo> ();
|
||||
readonly List<SourceFile> sources = new List<SourceFile> ();
|
||||
|
||||
public AssemblyInfo (byte[] assembly, byte[] pdb)
|
||||
{
|
||||
lock (typeof (AssemblyInfo)) {
|
||||
this.id = ++next_id;
|
||||
}
|
||||
|
||||
ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/);
|
||||
if (pdb != null) {
|
||||
rp.ReadSymbols = true;
|
||||
rp.SymbolReaderProvider = new PortablePdbReaderProvider ();
|
||||
rp.SymbolStream = new MemoryStream (pdb);
|
||||
}
|
||||
|
||||
rp.InMemory = true;
|
||||
|
||||
this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp);
|
||||
|
||||
Populate ();
|
||||
}
|
||||
|
||||
public AssemblyInfo ()
|
||||
{
|
||||
}
|
||||
|
||||
void Populate ()
|
||||
{
|
||||
var d2s = new Dictionary<Document, SourceFile> ();
|
||||
|
||||
Func<Document, SourceFile> get_src = (doc) => {
|
||||
if (doc == null)
|
||||
return null;
|
||||
if (d2s.ContainsKey (doc))
|
||||
return d2s [doc];
|
||||
var src = new SourceFile (this, sources.Count, doc);
|
||||
sources.Add (src);
|
||||
d2s [doc] = src;
|
||||
return src;
|
||||
};
|
||||
|
||||
foreach (var m in image.GetTypes ().SelectMany (t => t.Methods)) {
|
||||
Document first_doc = null;
|
||||
foreach (var sp in m.DebugInformation.SequencePoints) {
|
||||
if (first_doc == null) {
|
||||
first_doc = sp.Document;
|
||||
} else if (first_doc != sp.Document) {
|
||||
//FIXME this is needed for (c)ctors in corlib
|
||||
throw new Exception ($"Cant handle multi-doc methods in {m}");
|
||||
}
|
||||
}
|
||||
|
||||
var src = get_src (first_doc);
|
||||
var mi = new MethodInfo (this, m, src);
|
||||
int mt = (int)m.MetadataToken.RID;
|
||||
this.methods [mt] = mi;
|
||||
if (src != null)
|
||||
src.AddMethod (mi);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<SourceFile> Sources {
|
||||
get { return 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 (int token)
|
||||
{
|
||||
return methods [token];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class SourceFile {
|
||||
HashSet<MethodInfo> methods;
|
||||
AssemblyInfo assembly;
|
||||
int id;
|
||||
Document doc;
|
||||
|
||||
internal SourceFile (AssemblyInfo assembly, int id, Document doc)
|
||||
{
|
||||
this.methods = new HashSet<MethodInfo> ();
|
||||
this.assembly = assembly;
|
||||
this.id = id;
|
||||
this.doc = doc;
|
||||
}
|
||||
|
||||
internal void AddMethod (MethodInfo mi)
|
||||
{
|
||||
this.methods.Add (mi);
|
||||
}
|
||||
public string FileName => Path.GetFileName (doc.Url);
|
||||
public string Url => $"dotnet://{assembly.Name}/{FileName}";
|
||||
public string DocHashCode => "abcdee" + id;
|
||||
public SourceId SourceId => new SourceId (assembly.Id, this.id);
|
||||
public string LocalPath => doc.Url;
|
||||
|
||||
public IEnumerable<MethodInfo> Methods => this.methods;
|
||||
}
|
||||
|
||||
internal class DebugStore {
|
||||
List<AssemblyInfo> assemblies = new List<AssemblyInfo> ();
|
||||
|
||||
public DebugStore (string[] loaded_files)
|
||||
{
|
||||
bool MatchPdb (string asm, string pdb) {
|
||||
return Path.ChangeExtension (asm, "pdb") == pdb;
|
||||
}
|
||||
|
||||
var asm_files = new List<string> ();
|
||||
var pdb_files = new List<string> ();
|
||||
foreach (var f in loaded_files) {
|
||||
var file_name = f.ToLower ();
|
||||
if (file_name.EndsWith (".pdb", StringComparison.Ordinal))
|
||||
pdb_files.Add (file_name);
|
||||
else
|
||||
asm_files.Add (file_name);
|
||||
}
|
||||
|
||||
//FIXME make this parallel
|
||||
foreach (var p in asm_files) {
|
||||
var pdb = pdb_files.FirstOrDefault (n => MatchPdb (p, n));
|
||||
HttpClient h = new HttpClient ();
|
||||
var assembly_bytes = h.GetByteArrayAsync (p).Result;
|
||||
byte[] pdb_bytes = null;
|
||||
if (pdb != null)
|
||||
pdb_bytes = h.GetByteArrayAsync (pdb).Result;
|
||||
|
||||
this.assemblies.Add (new AssemblyInfo (assembly_bytes, pdb_bytes));
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<SourceFile> AllSources ()
|
||||
{
|
||||
foreach (var a in assemblies) {
|
||||
foreach (var s in a.Sources)
|
||||
yield return s;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public SourceFile GetFileById (SourceId id)
|
||||
{
|
||||
return AllSources ().FirstOrDefault (f => f.SourceId.Equals (id));
|
||||
}
|
||||
|
||||
public AssemblyInfo GetAssemblyByName (string name)
|
||||
{
|
||||
return assemblies.FirstOrDefault (a => a.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
/*
|
||||
Matching logic here is hilarious and it goes like this:
|
||||
We inject one line at the top of all sources to make it easy to identify them [1].
|
||||
V8 uses zero based indexing for both line and column.
|
||||
PPDBs uses one based indexing for both line and column.
|
||||
Which means that:
|
||||
- for lines, values are already adjusted (v8 numbers come +1 due to the injected line)
|
||||
- for columns, we need to +1 the v8 numbers
|
||||
[1] It's so we can deal with the Runtime.compileScript ide cmd
|
||||
*/
|
||||
static bool Match (SequencePoint sp, SourceLocation start, SourceLocation end)
|
||||
{
|
||||
if (start.Line > sp.StartLine)
|
||||
return false;
|
||||
if ((start.Column + 1) > sp.StartColumn && start.Line == sp.StartLine)
|
||||
return false;
|
||||
|
||||
if (end.Line < sp.EndLine)
|
||||
return false;
|
||||
|
||||
if ((end.Column + 1) < sp.EndColumn && end.Line == sp.EndLine)
|
||||
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)
|
||||
return null;
|
||||
var src_id = start.Id;
|
||||
|
||||
var doc = GetFileById (src_id);
|
||||
|
||||
var res = new List<SourceLocation> ();
|
||||
foreach (var m in doc.Methods) {
|
||||
foreach (var sp in m.methodDef.DebugInformation.SequencePoints) {
|
||||
if (Match (sp, start, end))
|
||||
res.Add (new SourceLocation (m, sp));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/*
|
||||
Matching logic here is hilarious and it goes like this:
|
||||
We inject one line at the top of all sources to make it easy to identify them [1].
|
||||
V8 uses zero based indexing for both line and column.
|
||||
PPDBs uses one based indexing for both line and column.
|
||||
Which means that:
|
||||
- for lines, values are already adjusted (v8 numbers come + 1 due to the injected line)
|
||||
- for columns, we need to +1 the v8 numbers
|
||||
[1] It's so we can deal with the Runtime.compileScript ide cmd
|
||||
*/
|
||||
static bool Match (SequencePoint sp, int line, int column)
|
||||
{
|
||||
if (sp.StartLine > line || sp.EndLine < line)
|
||||
return false;
|
||||
|
||||
//Chrome sends a zero column even if getPossibleBreakpoints say something else
|
||||
if (column == 0)
|
||||
return true;
|
||||
|
||||
if (sp.StartColumn > (column + 1) && sp.StartLine == line)
|
||||
return false;
|
||||
|
||||
if (sp.EndColumn < (column + 1) && sp.EndLine == line)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public SourceLocation FindBestBreakpoint (BreakPointRequest req)
|
||||
{
|
||||
var asm = this.assemblies.FirstOrDefault (a => a.Name == req.Assembly);
|
||||
var src = asm.Sources.FirstOrDefault (s => s.FileName == req.File);
|
||||
|
||||
foreach (var m in src.Methods) {
|
||||
foreach (var sp in m.methodDef.DebugInformation.SequencePoints) {
|
||||
//FIXME handle multi doc methods
|
||||
if (Match (sp, req.Line, req.Column))
|
||||
return new SourceLocation (m, sp);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string ToUrl (SourceLocation location)
|
||||
{
|
||||
return GetFileById (location.Id).Url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,590 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WsProxy {
|
||||
|
||||
internal class MonoCommands {
|
||||
public const string GET_CALL_STACK = "MONO.mono_wasm_get_call_stack()";
|
||||
public const string IS_RUNTIME_READY_VAR = "MONO.mono_wasm_runtime_is_ready";
|
||||
public const string START_SINGLE_STEPPING = "MONO.mono_wasm_start_single_stepping({0})";
|
||||
public const string GET_SCOPE_VARIABLES = "MONO.mono_wasm_get_variables({0}, [ {1} ])";
|
||||
public const string SET_BREAK_POINT = "MONO.mono_wasm_set_breakpoint(\"{0}\", {1}, {2})";
|
||||
public const string GET_LOADED_FILES = "MONO.mono_wasm_get_loaded_files()";
|
||||
public const string CLEAR_ALL_BREAKPOINTS = "MONO.mono_wasm_clear_all_breakpoints()";
|
||||
}
|
||||
|
||||
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 LocalId { get; private set; }
|
||||
public int RemoteId { get; set; }
|
||||
public BreakPointState State { get; set; }
|
||||
|
||||
public Breakpoint (SourceLocation loc, int localId, BreakPointState state)
|
||||
{
|
||||
this.Location = loc;
|
||||
this.LocalId = localId;
|
||||
this.State = state;
|
||||
}
|
||||
}
|
||||
|
||||
enum BreakPointState {
|
||||
Active,
|
||||
Disabled,
|
||||
Pending
|
||||
}
|
||||
|
||||
enum StepKind {
|
||||
Into,
|
||||
Out,
|
||||
Over
|
||||
}
|
||||
|
||||
internal class MonoProxy : WsProxy {
|
||||
DebugStore store;
|
||||
List<Breakpoint> breakpoints = new List<Breakpoint> ();
|
||||
List<Frame> current_callstack;
|
||||
bool runtime_ready;
|
||||
int local_breakpoint_id;
|
||||
int ctx_id;
|
||||
JObject aux_ctx_data;
|
||||
|
||||
public MonoProxy () { }
|
||||
|
||||
protected override async Task<bool> AcceptEvent (string method, JObject args, CancellationToken token)
|
||||
{
|
||||
switch (method) {
|
||||
case "Runtime.executionContextCreated": {
|
||||
var ctx = args? ["context"];
|
||||
var aux_data = ctx? ["auxData"] as JObject;
|
||||
if (aux_data != null) {
|
||||
var is_default = aux_data ["isDefault"]?.Value<bool> ();
|
||||
if (is_default == true) {
|
||||
var ctx_id = ctx ["id"].Value<int> ();
|
||||
await OnDefaultContext (ctx_id, aux_data, token);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
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") {
|
||||
await OnBreakPointHit (args, token);
|
||||
return true;
|
||||
}
|
||||
if (top_func == MonoConstants.RUNTIME_IS_READY) {
|
||||
await OnRuntimeReady (token);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Debugger.scriptParsed":{
|
||||
if (args?["url"]?.Value<string> ()?.StartsWith ("wasm://") == true) {
|
||||
// Console.WriteLine ("ignoring wasm event");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
protected override async Task<bool> AcceptCommand (int id, string method, JObject args, CancellationToken token)
|
||||
{
|
||||
switch (method) {
|
||||
case "Debugger.getScriptSource": {
|
||||
var script_id = args? ["scriptId"]?.Value<string> ();
|
||||
if (script_id.StartsWith ("dotnet://", StringComparison.InvariantCultureIgnoreCase)) {
|
||||
OnGetScriptSource (id, script_id, token);
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "Runtime.compileScript": {
|
||||
var exp = args? ["expression"]?.Value<string> ();
|
||||
if (exp.StartsWith ("//dotnet:", StringComparison.InvariantCultureIgnoreCase)) {
|
||||
OnCompileDotnetScript (id, token);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "Debugger.getPossibleBreakpoints": {
|
||||
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)
|
||||
return GetPossibleBreakpoints (id, start, end, token);
|
||||
break;
|
||||
}
|
||||
|
||||
case "Debugger.setBreakpointByUrl": {
|
||||
Info ($"BP req {args}");
|
||||
var bp_req = BreakPointRequest.Parse (args);
|
||||
if (bp_req != null) {
|
||||
await SetBreakPoint (id, bp_req, token);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "Debugger.resume": {
|
||||
await OnResume (token);
|
||||
break;
|
||||
}
|
||||
|
||||
case "Debugger.stepInto": {
|
||||
if (this.current_callstack != null) {
|
||||
await Step (id, StepKind.Into, token);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "Debugger.stepOut": {
|
||||
if (this.current_callstack != null) {
|
||||
await Step (id, StepKind.Out, token);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "Debugger.stepOver": {
|
||||
if (this.current_callstack != null) {
|
||||
await Step (id, StepKind.Over, token);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "Runtime.getProperties": {
|
||||
var objId = args? ["objectId"]?.Value<string> ();
|
||||
if (objId.StartsWith ("dotnet:scope:", StringComparison.InvariantCulture)) {
|
||||
await GetScopeProperties (id, int.Parse (objId.Substring ("dotnet:scope:".Length)), token);
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async Task OnRuntimeReady (CancellationToken token)
|
||||
{
|
||||
Info ("RUNTIME READY, PARTY TIME");
|
||||
await RuntimeReady (token);
|
||||
await SendCommand ("Debugger.resume", new JObject (), token);
|
||||
}
|
||||
|
||||
async Task OnBreakPointHit (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 o = JObject.FromObject (new {
|
||||
expression = MonoCommands.GET_CALL_STACK,
|
||||
objectGroup = "mono_debugger",
|
||||
includeCommandLineAPI = false,
|
||||
silent = false,
|
||||
returnByValue = true
|
||||
});
|
||||
|
||||
var orig_callframes = args? ["callFrames"]?.Values<JObject> ();
|
||||
var res = await SendCommand ("Runtime.evaluate", o, token);
|
||||
|
||||
if (res.IsErr) {
|
||||
//Give up and send the original call stack
|
||||
SendEvent ("Debugger.paused", args, token);
|
||||
return;
|
||||
}
|
||||
|
||||
//step one, figure out where did we hit
|
||||
//lol no, fuck it, let's use fake data
|
||||
var res_value = res.Value? ["result"]? ["value"];
|
||||
if (res_value == null || res_value is JValue) {
|
||||
//Give up and send the original call stack
|
||||
SendEvent ("Debugger.paused", args, token);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug ($"call stack (err is {res.Error} value is:\n{res.Value}");
|
||||
var bp_id = res_value? ["breakpoint_id"]?.Value<int> ();
|
||||
Debug ($"We just hit bp {bp_id}");
|
||||
if (!bp_id.HasValue) {
|
||||
//Give up and send the original call stack
|
||||
SendEvent ("Debugger.paused", args, token);
|
||||
return;
|
||||
}
|
||||
var bp = this.breakpoints.FirstOrDefault (b => b.RemoteId == bp_id.Value);
|
||||
|
||||
var src = bp == null ? null : store.GetFileById (bp.Location.Id);
|
||||
|
||||
var callFrames = new List<JObject> ();
|
||||
foreach (var f in orig_callframes) {
|
||||
var function_name = f ["functionName"]?.Value<string> ();
|
||||
var url = f ["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) {
|
||||
var il_pos = mono_frame ["il_pos"].Value<int> ();
|
||||
var method_token = mono_frame ["method_token"].Value<int> ();
|
||||
var assembly_name = mono_frame ["assembly_name"].Value<string> ();
|
||||
|
||||
var asm = store.GetAssemblyByName (assembly_name);
|
||||
var method = asm.GetMethodByToken (method_token);
|
||||
var location = method.GetLocationByIl (il_pos);
|
||||
|
||||
Info ($"frame il offset: {il_pos} method token: {method_token} assembly name: {assembly_name}");
|
||||
Info ($"\tmethod {method.Name} location: {location}");
|
||||
frames.Add (new Frame (method, location, frame_id));
|
||||
|
||||
callFrames.Add (JObject.FromObject (new {
|
||||
functionName = method.Name,
|
||||
|
||||
functionLocation = method.StartLocation.ToJObject (),
|
||||
|
||||
location = location.ToJObject (),
|
||||
|
||||
url = store.ToUrl (location),
|
||||
|
||||
scopeChain = new [] {
|
||||
new {
|
||||
type = "local",
|
||||
@object = new {
|
||||
@type = "object",
|
||||
className = "Object",
|
||||
description = "Object",
|
||||
objectId = $"dotnet:scope:{frame_id}"
|
||||
},
|
||||
name = method.Name,
|
||||
startLocation = method.StartLocation.ToJObject (),
|
||||
endLocation = method.EndLocation.ToJObject (),
|
||||
}
|
||||
},
|
||||
|
||||
@this = new {
|
||||
}
|
||||
}));
|
||||
|
||||
++frame_id;
|
||||
this.current_callstack = frames;
|
||||
}
|
||||
} else if (!url.StartsWith ("wasm://wasm/", StringComparison.InvariantCulture)) {
|
||||
callFrames.Add (f);
|
||||
}
|
||||
}
|
||||
|
||||
var bp_list = new string [bp == null ? 0 : 1];
|
||||
if (bp != null)
|
||||
bp_list [0] = $"dotnet:{bp.LocalId}";
|
||||
|
||||
o = JObject.FromObject (new {
|
||||
callFrames = callFrames,
|
||||
reason = "other", //other means breakpoint
|
||||
hitBreakpoints = bp_list,
|
||||
});
|
||||
|
||||
SendEvent ("Debugger.paused", o, token);
|
||||
}
|
||||
|
||||
async Task OnDefaultContext (int ctx_id, JObject aux_data, CancellationToken token)
|
||||
{
|
||||
Debug ("Default context created, clearing state and sending events");
|
||||
|
||||
//reset all bps
|
||||
foreach (var b in this.breakpoints){
|
||||
b.State = BreakPointState.Pending;
|
||||
}
|
||||
this.runtime_ready = false;
|
||||
|
||||
var o = JObject.FromObject (new {
|
||||
expression = MonoCommands.IS_RUNTIME_READY_VAR,
|
||||
objectGroup = "mono_debugger",
|
||||
includeCommandLineAPI = false,
|
||||
silent = false,
|
||||
returnByValue = true
|
||||
});
|
||||
this.ctx_id = ctx_id;
|
||||
this.aux_ctx_data = aux_data;
|
||||
|
||||
Debug ("checking if the runtime is ready");
|
||||
var res = await SendCommand ("Runtime.evaluate", o, token);
|
||||
var is_ready = res.Value? ["result"]? ["value"]?.Value<bool> ();
|
||||
//Debug ($"\t{is_ready}");
|
||||
if (is_ready.HasValue && is_ready.Value == true) {
|
||||
Debug ("RUNTIME LOOK READY. GO TIME!");
|
||||
await RuntimeReady (token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async Task OnResume (CancellationToken token)
|
||||
{
|
||||
//discard frames
|
||||
this.current_callstack = null;
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
async Task Step (int msg_id, StepKind kind, CancellationToken token)
|
||||
{
|
||||
|
||||
var o = JObject.FromObject (new {
|
||||
expression = string.Format (MonoCommands.START_SINGLE_STEPPING, (int)kind),
|
||||
objectGroup = "mono_debugger",
|
||||
includeCommandLineAPI = false,
|
||||
silent = false,
|
||||
returnByValue = true,
|
||||
});
|
||||
|
||||
var res = await SendCommand ("Runtime.evaluate", o, token);
|
||||
|
||||
SendResponse (msg_id, Result.Ok (new JObject ()), token);
|
||||
|
||||
this.current_callstack = null;
|
||||
|
||||
await SendCommand ("Debugger.resume", new JObject (), token);
|
||||
}
|
||||
|
||||
async Task GetScopeProperties (int msg_id, int scope_id, CancellationToken token)
|
||||
{
|
||||
var scope = this.current_callstack.FirstOrDefault (s => s.Id == scope_id);
|
||||
var vars = scope.Method.GetLiveVarsAt (scope.Location.CliLocation.Offset);
|
||||
|
||||
|
||||
var var_ids = string.Join (",", vars.Select (v => v.Index));
|
||||
|
||||
var o = JObject.FromObject (new {
|
||||
expression = string.Format (MonoCommands.GET_SCOPE_VARIABLES, scope.Id, var_ids),
|
||||
objectGroup = "mono_debugger",
|
||||
includeCommandLineAPI = false,
|
||||
silent = false,
|
||||
returnByValue = true,
|
||||
});
|
||||
|
||||
var res = await SendCommand ("Runtime.evaluate", o, token);
|
||||
|
||||
//if we fail we just buble that to the IDE (and let it panic over it)
|
||||
if (res.IsErr) {
|
||||
SendResponse (msg_id, res, token);
|
||||
return;
|
||||
}
|
||||
|
||||
var values = res.Value? ["result"]? ["value"]?.Values<JObject> ().ToArray ();
|
||||
|
||||
var var_list = new List<JObject> ();
|
||||
for (int i = 0; i < vars.Length; ++i) {
|
||||
var_list.Add (JObject.FromObject (new {
|
||||
name = vars [i].Name,
|
||||
value = values [i] ["value"]
|
||||
}));
|
||||
|
||||
}
|
||||
o = JObject.FromObject (new {
|
||||
result = var_list
|
||||
});
|
||||
|
||||
SendResponse (msg_id, Result.Ok (o), token);
|
||||
}
|
||||
|
||||
async Task<Result> EnableBreakPoint (Breakpoint bp, CancellationToken token)
|
||||
{
|
||||
var asm_name = bp.Location.CliLocation.Method.Assembly.Name.ToLower ();
|
||||
var method_token = bp.Location.CliLocation.Method.Token;
|
||||
var il_offset = bp.Location.CliLocation.Offset;
|
||||
|
||||
var o = JObject.FromObject (new {
|
||||
expression = string.Format (MonoCommands.SET_BREAK_POINT, asm_name, method_token, il_offset),
|
||||
objectGroup = "mono_debugger",
|
||||
includeCommandLineAPI = false,
|
||||
silent = false,
|
||||
returnByValue = true,
|
||||
});
|
||||
|
||||
var res = await SendCommand ("Runtime.evaluate", o, token);
|
||||
var ret_code = res.Value? ["result"]? ["value"]?.Value<int> ();
|
||||
|
||||
if (ret_code.HasValue) {
|
||||
bp.RemoteId = ret_code.Value;
|
||||
bp.State = BreakPointState.Active;
|
||||
//Debug ($"BP local id {bp.LocalId} enabled with remote id {bp.RemoteId}");
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async Task RuntimeReady (CancellationToken token)
|
||||
{
|
||||
|
||||
var o = JObject.FromObject (new {
|
||||
expression = MonoCommands.GET_LOADED_FILES,
|
||||
objectGroup = "mono_debugger",
|
||||
includeCommandLineAPI = false,
|
||||
silent = false,
|
||||
returnByValue = true,
|
||||
});
|
||||
var loaded_pdbs = await SendCommand ("Runtime.evaluate", o, token);
|
||||
var the_value = loaded_pdbs.Value? ["result"]? ["value"];
|
||||
var the_pdbs = the_value?.ToObject<string[]> ();
|
||||
this.store = new DebugStore (the_pdbs);
|
||||
|
||||
foreach (var s in store.AllSources ()) {
|
||||
var ok = JObject.FromObject (new {
|
||||
scriptId = s.SourceId.ToString (),
|
||||
url = s.Url,
|
||||
executionContextId = this.ctx_id,
|
||||
hash = s.DocHashCode,
|
||||
executionContextAuxData = this.aux_ctx_data
|
||||
});
|
||||
//Debug ($"\tsending {s.Url}");
|
||||
SendEvent ("Debugger.scriptParsed", ok, token);
|
||||
}
|
||||
|
||||
o = JObject.FromObject (new {
|
||||
expression = MonoCommands.CLEAR_ALL_BREAKPOINTS,
|
||||
objectGroup = "mono_debugger",
|
||||
includeCommandLineAPI = false,
|
||||
silent = false,
|
||||
returnByValue = true,
|
||||
});
|
||||
|
||||
var clear_result = await SendCommand ("Runtime.evaluate", o, token);
|
||||
if (clear_result.IsErr) {
|
||||
Debug ($"Failed to clear breakpoints due to {clear_result}");
|
||||
}
|
||||
|
||||
|
||||
runtime_ready = true;
|
||||
|
||||
foreach (var bp in breakpoints) {
|
||||
if (bp.State != BreakPointState.Pending)
|
||||
continue;
|
||||
var res = await EnableBreakPoint (bp, token);
|
||||
var ret_code = res.Value? ["result"]? ["value"]?.Value<int> ();
|
||||
|
||||
//if we fail we just buble that to the IDE (and let it panic over it)
|
||||
if (!ret_code.HasValue) {
|
||||
//FIXME figure out how to inform the IDE of that.
|
||||
Info ($"FAILED TO ENABLE BP {bp.LocalId}");
|
||||
bp.State = BreakPointState.Disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task SetBreakPoint (int msg_id, BreakPointRequest req, CancellationToken token)
|
||||
{
|
||||
var bp_loc = store.FindBestBreakpoint (req);
|
||||
Info ($"BP request for '{req}' runtime ready {runtime_ready}");
|
||||
|
||||
Breakpoint bp = null;
|
||||
if (!runtime_ready) {
|
||||
bp = new Breakpoint (bp_loc, local_breakpoint_id++, BreakPointState.Pending);
|
||||
} else {
|
||||
bp = new Breakpoint (bp_loc, local_breakpoint_id++, BreakPointState.Disabled);
|
||||
|
||||
var res = await EnableBreakPoint (bp, token);
|
||||
var ret_code = res.Value? ["result"]? ["value"]?.Value<int> ();
|
||||
|
||||
//if we fail we just buble that to the IDE (and let it panic over it)
|
||||
if (!ret_code.HasValue) {
|
||||
SendResponse (msg_id, res, token);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var locations = new List<JObject> ();
|
||||
|
||||
locations.Add (JObject.FromObject (new {
|
||||
scriptId = bp_loc.Id.ToString (),
|
||||
lineNumber = bp_loc.Line,
|
||||
columnNumber = bp_loc.Column
|
||||
}));
|
||||
|
||||
breakpoints.Add (bp);
|
||||
|
||||
var ok = JObject.FromObject (new {
|
||||
breakpointId = $"dotnet:{bp.LocalId}",
|
||||
locations = locations,
|
||||
});
|
||||
|
||||
SendResponse (msg_id, Result.Ok (ok), token);
|
||||
}
|
||||
|
||||
bool GetPossibleBreakpoints (int msg_id, SourceLocation start, SourceLocation end, CancellationToken token)
|
||||
{
|
||||
var bps = store.FindPossibleBreakpoints (start, end);
|
||||
if (bps == null)
|
||||
return false;
|
||||
|
||||
var loc = new List<JObject> ();
|
||||
foreach (var b in bps) {
|
||||
loc.Add (b.ToJObject ());
|
||||
}
|
||||
|
||||
var o = JObject.FromObject (new {
|
||||
locations = loc
|
||||
});
|
||||
|
||||
SendResponse (msg_id, Result.Ok (o), token);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnCompileDotnetScript (int msg_id, CancellationToken token)
|
||||
{
|
||||
var o = JObject.FromObject (new { });
|
||||
|
||||
SendResponse (msg_id, Result.Ok (o), token);
|
||||
|
||||
}
|
||||
|
||||
void OnGetScriptSource (int msg_id, string script_id, CancellationToken token)
|
||||
{
|
||||
var id = new SourceId (script_id);
|
||||
var src_file = store.GetFileById (id);
|
||||
|
||||
var res = new StringWriter ();
|
||||
res.WriteLine ($"//dotnet:{id}");
|
||||
|
||||
using (var f = new StreamReader (File.Open (src_file.LocalPath, FileMode.Open))) {
|
||||
res.Write (f.ReadToEnd ());
|
||||
}
|
||||
|
||||
var o = JObject.FromObject (new {
|
||||
scriptSource = res.ToString ()
|
||||
});
|
||||
|
||||
SendResponse (msg_id, Result.Ok (o), token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Running the proxy
|
||||
|
||||
Hack your way around, good luck
|
||||
|
||||
# TODO
|
||||
|
||||
Reconnects - reconsile with all existing breakpoints in the runtime
|
||||
F5 - recognize it and reset debuger state
|
||||
release object groups - we don't send for the groups we create nor do we intercept the fake ones we generate for the runtime
|
||||
Deal with the browser caching stuff across execution - cache invalidation and proper bundling of resources
|
||||
Figure out how to enable/disable debugging
|
||||
Do some target validation - right now we simply assume the debugee has a mono runtime in debug mode
|
||||
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace WsProxy
|
||||
{
|
||||
|
||||
internal class Startup {
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
public void ConfigureServices (IServiceCollection services)
|
||||
{
|
||||
services.AddRouting ();
|
||||
}
|
||||
|
||||
public static async Task ProxyMsg (string desc, WebSocket from, WebSocket to)
|
||||
{
|
||||
byte [] buff = new byte [4000];
|
||||
var mem = new MemoryStream ();
|
||||
while (true) {
|
||||
var result = await from.ReceiveAsync (new ArraySegment<byte> (buff), CancellationToken.None);
|
||||
if (result.MessageType == WebSocketMessageType.Close) {
|
||||
await to.SendAsync (new ArraySegment<byte> (mem.GetBuffer (), 0, (int)mem.Length), WebSocketMessageType.Close, true, CancellationToken.None);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.EndOfMessage) {
|
||||
mem.Write (buff, 0, result.Count);
|
||||
|
||||
var str = Encoding.UTF8.GetString (mem.GetBuffer (), 0, (int)mem.Length);
|
||||
|
||||
await to.SendAsync (new ArraySegment<byte> (mem.GetBuffer (), 0, (int)mem.Length), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
mem.SetLength (0);
|
||||
} else {
|
||||
mem.Write (buff, 0, result.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure (IApplicationBuilder app, IHostingEnvironment env)
|
||||
{
|
||||
//loggerFactory.AddConsole();
|
||||
//loggerFactory.AddDebug();
|
||||
//app.UseDeveloperExceptionPage ();
|
||||
|
||||
app.UseWebSockets (); app.UseRouter (router => {
|
||||
router.MapGet ("devtools/page/{pageId}", async context => {
|
||||
if (!context.WebSockets.IsWebSocketRequest) {
|
||||
context.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var proxy = new MonoProxy ();
|
||||
await proxy.Run (context);
|
||||
} catch (Exception e) {
|
||||
Console.WriteLine ("got exception {0}", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WsProxy {
|
||||
|
||||
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)
|
||||
{
|
||||
this.Value = result;
|
||||
this.Error = error;
|
||||
}
|
||||
|
||||
public static Result FromJson (JObject obj)
|
||||
{
|
||||
return new Result (obj ["result"] as JObject, obj ["error"] as JObject);
|
||||
}
|
||||
|
||||
public static Result Ok (JObject ok)
|
||||
{
|
||||
return new Result (ok, null);
|
||||
}
|
||||
|
||||
public static Result Err (JObject err)
|
||||
{
|
||||
return new Result (null, err);
|
||||
}
|
||||
|
||||
public JObject ToJObject (int id) {
|
||||
if (IsOk) {
|
||||
return JObject.FromObject (new {
|
||||
id = id,
|
||||
result = Value
|
||||
});
|
||||
} else {
|
||||
return JObject.FromObject (new {
|
||||
id = id,
|
||||
error = Error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WsQueue {
|
||||
Task current_send;
|
||||
List<byte []> pending;
|
||||
|
||||
public WebSocket Ws { get; private set; }
|
||||
public Task CurrentSend { get { return current_send; } }
|
||||
public WsQueue (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 ("WTF, current_send MUST BE NULL IF THERE'S no pending send");
|
||||
//Console.WriteLine ("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 ("WTF, current_send MUST BE NULL IF THERE'S no pending send");
|
||||
//Console.WriteLine ("sending more {0} bytes", pending[0].Length);
|
||||
current_send = Ws.SendAsync (new ArraySegment<byte> (pending [0]), WebSocketMessageType.Text, true, token);
|
||||
return current_send;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal class WsProxy {
|
||||
TaskCompletionSource<bool> side_exception = new TaskCompletionSource<bool> ();
|
||||
List<(int, TaskCompletionSource<Result>)> pending_cmds = new List<(int, TaskCompletionSource<Result>)> ();
|
||||
ClientWebSocket browser;
|
||||
WebSocket ide;
|
||||
int next_cmd_id;
|
||||
List<Task> pending_ops = new List<Task> ();
|
||||
List<WsQueue> queues = new List<WsQueue> ();
|
||||
|
||||
protected virtual Task<bool> AcceptEvent (string method, JObject args, CancellationToken token)
|
||||
{
|
||||
return Task.FromResult (false);
|
||||
}
|
||||
|
||||
protected virtual Task<bool> AcceptCommand (int id, string method, JObject args, CancellationToken token)
|
||||
{
|
||||
return Task.FromResult (false);
|
||||
}
|
||||
|
||||
Uri GetBrowserUri (string path)
|
||||
{
|
||||
return new Uri ("ws://localhost:9222" + path);
|
||||
}
|
||||
|
||||
async Task<string> ReadOne (WebSocket socket, CancellationToken token)
|
||||
{
|
||||
byte [] buff = new byte [4000];
|
||||
var mem = new MemoryStream ();
|
||||
while (true) {
|
||||
var result = await socket.ReceiveAsync (new ArraySegment<byte> (buff), token);
|
||||
if (result.MessageType == WebSocketMessageType.Close) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (result.EndOfMessage) {
|
||||
mem.Write (buff, 0, result.Count);
|
||||
return Encoding.UTF8.GetString (mem.GetBuffer (), 0, (int)mem.Length);
|
||||
} else {
|
||||
mem.Write (buff, 0, result.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
WsQueue GetQueueForSocket (WebSocket ws)
|
||||
{
|
||||
return queues.FirstOrDefault (q => q.Ws == ws);
|
||||
}
|
||||
|
||||
WsQueue GetQueueForTask (Task task) {
|
||||
return queues.FirstOrDefault (q => q.CurrentSend == task);
|
||||
}
|
||||
|
||||
void Send (WebSocket to, JObject o, CancellationToken token)
|
||||
{
|
||||
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 (string method, JObject args, CancellationToken token)
|
||||
{
|
||||
try {
|
||||
if (!await AcceptEvent (method, args, token)) {
|
||||
//Console.WriteLine ("proxy browser: {0}::{1}",method, args);
|
||||
SendEventInternal (method, args, token);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
side_exception.TrySetException (e);
|
||||
}
|
||||
}
|
||||
|
||||
async Task OnCommand (int id, string method, JObject args, CancellationToken token)
|
||||
{
|
||||
try {
|
||||
if (!await AcceptCommand (id, method, args, token)) {
|
||||
var res = await SendCommandInternal (method, args, token);
|
||||
SendResponseInternal (id, res, token);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
side_exception.TrySetException (e);
|
||||
}
|
||||
}
|
||||
|
||||
void OnResponse (int id, Result result)
|
||||
{
|
||||
//Console.WriteLine ("got id {0} res {1}", id, result);
|
||||
var idx = pending_cmds.FindIndex (e => e.Item1 == id);
|
||||
var item = pending_cmds [idx];
|
||||
pending_cmds.RemoveAt (idx);
|
||||
|
||||
item.Item2.SetResult (result);
|
||||
}
|
||||
|
||||
void ProcessBrowserMessage (string msg, CancellationToken token)
|
||||
{
|
||||
// Debug ($"browser: {msg}");
|
||||
var res = JObject.Parse (msg);
|
||||
|
||||
if (res ["id"] == null)
|
||||
pending_ops.Add (OnEvent (res ["method"].Value<string> (), res ["params"] as JObject, token));
|
||||
else
|
||||
OnResponse (res ["id"].Value<int> (), Result.FromJson (res));
|
||||
}
|
||||
|
||||
void ProcessIdeMessage (string msg, CancellationToken token)
|
||||
{
|
||||
var res = JObject.Parse (msg);
|
||||
|
||||
pending_ops.Add (OnCommand (res ["id"].Value<int> (), res ["method"].Value<string> (), res ["params"] as JObject, token));
|
||||
}
|
||||
|
||||
public async Task<Result> SendCommand (string method, JObject args, CancellationToken token) {
|
||||
Debug ($"sending command {method}: {args}");
|
||||
return await SendCommandInternal (method, args, token);
|
||||
}
|
||||
|
||||
Task<Result> SendCommandInternal (string method, JObject args, CancellationToken token)
|
||||
{
|
||||
int id = ++next_cmd_id;
|
||||
|
||||
var o = JObject.FromObject (new {
|
||||
id = id,
|
||||
method = method,
|
||||
@params = args
|
||||
});
|
||||
var tcs = new TaskCompletionSource<Result> ();
|
||||
//Console.WriteLine ("add cmd id {0}", id);
|
||||
pending_cmds.Add ((id, tcs));
|
||||
|
||||
Send (this.browser, o, token);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public void SendEvent (string method, JObject args, CancellationToken token)
|
||||
{
|
||||
//Debug ($"sending event {method}: {args}");
|
||||
SendEventInternal (method, args, token);
|
||||
}
|
||||
|
||||
void SendEventInternal (string method, JObject args, CancellationToken token)
|
||||
{
|
||||
var o = JObject.FromObject (new {
|
||||
method = method,
|
||||
@params = args
|
||||
});
|
||||
|
||||
Send (this.ide, o, token);
|
||||
}
|
||||
|
||||
public void SendResponse (int id, Result result, CancellationToken token)
|
||||
{
|
||||
//Debug ($"sending response: {id}: {result.ToJObject (id)}");
|
||||
SendResponseInternal (id, result, token);
|
||||
}
|
||||
|
||||
void SendResponseInternal (int id, Result result, CancellationToken token)
|
||||
{
|
||||
JObject o = result.ToJObject (id);
|
||||
|
||||
Send (this.ide, o, token);
|
||||
}
|
||||
|
||||
public async Task Run (HttpContext context)
|
||||
{
|
||||
var browserUri = GetBrowserUri (context.Request.Path.ToString ());
|
||||
Debug ("wsproxy start");
|
||||
using (this.ide = await context.WebSockets.AcceptWebSocketAsync ()) {
|
||||
Debug ("ide connected");
|
||||
queues.Add (new WsQueue (this.ide));
|
||||
using (this.browser = new ClientWebSocket ()) {
|
||||
this.browser.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan;
|
||||
await this.browser.ConnectAsync (browserUri, CancellationToken.None);
|
||||
queues.Add (new WsQueue (this.browser));
|
||||
|
||||
Debug ("client connected");
|
||||
var x = new CancellationTokenSource ();
|
||||
|
||||
pending_ops.Add (ReadOne (browser, x.Token));
|
||||
pending_ops.Add (ReadOne (ide, x.Token));
|
||||
pending_ops.Add (side_exception.Task);
|
||||
|
||||
try {
|
||||
while (!x.IsCancellationRequested) {
|
||||
var task = await Task.WhenAny (pending_ops);
|
||||
//Console.WriteLine ("pump {0} {1}", task, pending_ops.IndexOf (task));
|
||||
if (task == pending_ops [0]) {
|
||||
var msg = ((Task<string>)task).Result;
|
||||
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;
|
||||
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 {
|
||||
//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) {
|
||||
Debug ($"got exception {e}");
|
||||
//throw;
|
||||
} finally {
|
||||
x.Cancel ();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void Debug (string msg)
|
||||
{
|
||||
Console.WriteLine (msg);
|
||||
}
|
||||
|
||||
protected void Info (string msg)
|
||||
{
|
||||
Console.WriteLine (msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue