Include Mono Debug proxy code in Server project (temporarily until it's shipped as a NuGet package)

This commit is contained in:
Steve Sanderson 2018-07-04 11:49:57 +01:00
parent f483e6293e
commit c881a63a78
7 changed files with 1547 additions and 1 deletions

View File

@ -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>

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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);
}
});
});
}
}
}

View File

@ -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);
}
}
}