From 691910c67d80dec3fca30a1dd3a296bfa900339c Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Sun, 9 Aug 2020 11:13:09 -0700 Subject: [PATCH] Use MonoProxy package for debugging --- eng/Dependencies.props | 1 + eng/Version.Details.xml | 4 + .../DebugProxy/src/DebugProxyOptions.cs | 10 - .../DebugProxy/src/Hosting/DebugProxyHost.cs | 65 -- ...e.Components.WebAssembly.DebugProxy.csproj | 30 - .../src/MonoDebugProxy/ws-proxy/DebugStore.cs | 843 ----------------- .../MonoDebugProxy/ws-proxy/DevToolsHelper.cs | 298 ------ .../MonoDebugProxy/ws-proxy/DevToolsProxy.cs | 337 ------- .../ws-proxy/EvaluateExpression.cs | 182 ---- .../src/MonoDebugProxy/ws-proxy/MonoProxy.cs | 885 ------------------ .../WebAssembly/DebugProxy/src/Program.cs | 71 -- .../WebAssembly/DebugProxy/src/Startup.cs | 45 - .../Server/src/DebugProxyLauncher.cs | 4 +- ...tCore.Components.WebAssembly.Server.csproj | 23 +- .../src/TargetPickerUi.cs | 25 +- ...semblyNetDebugProxyAppBuilderExtensions.cs | 6 +- 16 files changed, 28 insertions(+), 2801 deletions(-) delete mode 100644 src/Components/WebAssembly/DebugProxy/src/DebugProxyOptions.cs delete mode 100644 src/Components/WebAssembly/DebugProxy/src/Hosting/DebugProxyHost.cs delete mode 100644 src/Components/WebAssembly/DebugProxy/src/Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj delete mode 100644 src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DebugStore.cs delete mode 100644 src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs delete mode 100644 src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs delete mode 100644 src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs delete mode 100644 src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs delete mode 100644 src/Components/WebAssembly/DebugProxy/src/Program.cs delete mode 100644 src/Components/WebAssembly/DebugProxy/src/Startup.cs rename src/Components/WebAssembly/{DebugProxy => Server}/src/TargetPickerUi.cs (91%) diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 1543d4c13b..d86ab879db 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -124,6 +124,7 @@ and are generated based on the last package release. + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index b351d947e8..bf00659030 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -306,6 +306,10 @@ https://github.com/dotnet/runtime 1dfd9438149f74ae11918a7b0709b8d58c61443f + + https://github.com/dotnet/runtime + 6cc7cffaff2e0413ee84d9a7736f9ae1aa9a3f12 + diff --git a/src/Components/WebAssembly/DebugProxy/src/DebugProxyOptions.cs b/src/Components/WebAssembly/DebugProxy/src/DebugProxyOptions.cs deleted file mode 100644 index 70a76258c6..0000000000 --- a/src/Components/WebAssembly/DebugProxy/src/DebugProxyOptions.cs +++ /dev/null @@ -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; } - } -} diff --git a/src/Components/WebAssembly/DebugProxy/src/Hosting/DebugProxyHost.cs b/src/Components/WebAssembly/DebugProxy/src/Hosting/DebugProxyHost.cs deleted file mode 100644 index 4edbddc2f5..0000000000 --- a/src/Components/WebAssembly/DebugProxy/src/Hosting/DebugProxyHost.cs +++ /dev/null @@ -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 - { - /// - /// Creates a custom HostBuilder for the DebugProxyLauncher so that we can inject - /// only the needed configurations. - /// - /// Command line arguments passed in - /// Host where browser is listening for debug connections - /// - 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(); - - // 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; - - } - } -} diff --git a/src/Components/WebAssembly/DebugProxy/src/Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj b/src/Components/WebAssembly/DebugProxy/src/Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj deleted file mode 100644 index 4a727c739d..0000000000 --- a/src/Components/WebAssembly/DebugProxy/src/Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - $(DefaultNetCoreTargetFramework) - Exe - Microsoft.AspNetCore.Components.WebAssembly.DebugProxy - true - Debug proxy for use when building Blazor applications. - - false - $(NoWarn);CS0649 - true - - - - - - - - - - - - - - - - - - diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DebugStore.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DebugStore.cs deleted file mode 100644 index 1b9568de80..0000000000 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DebugStore.cs +++ /dev/null @@ -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 Locations { get; } = new List (); - - public override string ToString () - => $"BreakpointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}"; - - public object AsSetBreakpointByUrlResponse (IEnumerable 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 (); - if (url == null) { - var urlRegex = request?["urlRegex"].Value(); - 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 (); - var column = request? ["columnNumber"]?.Value (); - - 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 (), out var id)) - return null; - - var line = obj ["lineNumber"]?.Value (); - var column = obj ["columnNumber"]?.Value (); - if (id == null || line == null || column == null) - return null; - - return new SourceLocation (id, line.Value, column.Value); - } - - - internal class LocationComparer : EqualityComparer - { - 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 (); - - 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 methods; - - public TypeInfo (AssemblyInfo assembly, TypeDefinition type) { - this.assembly = assembly; - this.type = type; - methods = new List (); - } - - public string Name => type.Name; - public string FullName => type.FullName; - public List Methods => methods; - - public override string ToString () => "TypeInfo('" + FullName + "')"; - } - - class AssemblyInfo { - static int next_id; - ModuleDefinition image; - readonly int id; - readonly ILogger logger; - Dictionary methods = new Dictionary (); - Dictionary sourceLinkMappings = new Dictionary(); - Dictionary typesByName = new Dictionary (); - readonly List sources = new List(); - 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 (); - - 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> (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 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 methods; - AssemblyInfo assembly; - int id; - Document doc; - - internal SourceFile (AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri) - { - this.methods = new Dictionary (); - 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 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 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 (); - } - - public async Task 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 assemblies = new List (); - 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 Data { get; set; } - } - - public async IAsyncEnumerable 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 (); - var pdb_files = new List (); - 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 steps = new List (); - 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 (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 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 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 (); - 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 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 : ""; - } -} diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs deleted file mode 100644 index 871c10de9b..0000000000 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs +++ /dev/null @@ -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(), 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 (), "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 BreakpointRequests { get; } = new Dictionary (); - - public TaskCompletionSource ready = null; - public bool IsRuntimeReady => ready != null && ready.Task.IsCompleted; - - public int Id { get; set; } - public object AuxData { get; set; } - - public List CallStack { get; set; } - - internal DebugStore store; - public TaskCompletionSource Source { get; } = new TaskCompletionSource (); - - public Dictionary LocalsCache = new Dictionary (); - - public DebugStore Store { - get { - if (store == null || !Source.Task.IsCompleted) - return null; - - return store; - } - } - - public void ClearState () - { - CallStack = null; - LocalsCache.Clear (); - } - - } -} diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs deleted file mode 100644 index 5eac86d124..0000000000 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs +++ /dev/null @@ -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 pending; - - public WebSocket Ws { get; private set; } - public Task CurrentSend { get { return current_send; } } - public DevToolsQueue (WebSocket sock) - { - this.Ws = sock; - pending = new List (); - } - - 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 (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 (pending [0]), WebSocketMessageType.Text, true, token); - return current_send; - } - return null; - } - } - - internal class DevToolsProxy { - TaskCompletionSource side_exception = new TaskCompletionSource (); - TaskCompletionSource client_initiated_close = new TaskCompletionSource (); - Dictionary> pending_cmds = new Dictionary> (); - ClientWebSocket browser; - WebSocket ide; - int next_cmd_id; - List pending_ops = new List (); - List queues = new List (); - - protected readonly ILogger logger; - - public DevToolsProxy (ILoggerFactory loggerFactory) - { - logger = loggerFactory.CreateLogger(); - } - - protected virtual Task AcceptEvent (SessionId sessionId, string method, JObject args, CancellationToken token) - { - return Task.FromResult (false); - } - - protected virtual Task AcceptCommand (MessageId id, string method, JObject args, CancellationToken token) - { - return Task.FromResult (false); - } - - async Task 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 (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 ()), res ["method"].Value (), res ["params"] as JObject, token)); - else - OnResponse (new MessageId (res ["sessionId"]?.Value (), res ["id"].Value ()), 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 (), res ["id"].Value ()), - res ["method"].Value (), - res ["params"] as JObject, token)); - } - } - - internal async Task SendCommand (SessionId id, string method, JObject args, CancellationToken token) { - //Log ("verbose", $"sending command {method}: {args}"); - return await SendCommandInternal (id, method, args, token); - } - - Task 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 (); - - 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)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)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)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)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; - } - } - } -} diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs deleted file mode 100644 index fb7e776ed9..0000000000 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs +++ /dev/null @@ -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 thisExpressions = new List (); - 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 variables = new List (); - public List thisList = new List (); - public List methodCall = new List (); - public List values = new List (); - - 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 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 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; - } - } -} diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs deleted file mode 100644 index 1b26dd93fd..0000000000 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs +++ /dev/null @@ -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 sessions = new HashSet (); - Dictionary contexts = new Dictionary (); - - 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 SendMonoCommand (SessionId id, MonoCommands cmd, CancellationToken token) - => SendCommand (id, "Runtime.evaluate", JObject.FromObject (cmd), token); - - protected override async Task 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 (); - if (aux_data != null) { - var is_default = aux_data ["isDefault"]?.Value (); - 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 (); - - 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 () ?? ""; - - 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 IsRuntimeAlreadyReadyAlready (SessionId sessionId, CancellationToken token) - { - var res = await SendMonoCommand (sessionId, MonoCommands.IsRuntimeReady (), token); - return res.Value? ["result"]? ["value"]?.Value () ?? false; - } - - static int bpIdGenerator; - - protected override async Task 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 (); - return await OnGetScriptSource (id, script, token); - } - - case "Runtime.compileScript": { - var exp = args? ["expression"]?.Value (); - 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(); - 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 (), 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 typeName = args ["typeName"]?.Value (); - string methodName = args ["methodName"]?.Value (); - 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 () ?? 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 () ?? 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 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 (); - 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 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 (); - 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 (); - 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 (); - foreach (var frame in orig_callframes) { - var function_name = frame ["functionName"]?.Value (); - var url = frame ["url"]?.Value (); - if ("mono_wasm_fire_bp" == function_name || "_mono_wasm_fire_bp" == function_name) { - var frames = new List (); - int frame_id = 0; - var the_mono_frames = res.Value? ["result"]? ["value"]? ["frames"]?.Values (); - - foreach (var mono_frame in the_mono_frames) { - ++frame_id; - var il_pos = mono_frame ["il_pos"].Value (); - var method_token = mono_frame ["method_token"].Value (); - var assembly_name = mono_frame ["assembly_name"].Value (); - - // This can be different than `method.Name`, like in case of generic methods - var method_name = mono_frame ["method_name"]?.Value (); - - 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 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 (); - - 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 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 ()?.ToArray (); - thisValue = scope_values?.FirstOrDefault (v => v ["name"]?.Value () == "this"); - - if (!only_search_on_this) { - if (thisValue != null && expression == "this") - return thisValue; - - var value = scope_values.SingleOrDefault (sv => sv ["name"]?.Value () == 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 ().ToArray (); - var foundValue = scope_values.FirstOrDefault (v => v ["name"].Value () == expression); - if (foundValue != null) { - foundValue["fromThis"] = true; - context.LocalsCache[foundValue ["name"].Value ()] = foundValue; - return foundValue; - } - } - return null; - } - - async Task 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 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 ().ToArray (); - - if(values == null) - return Result.OkFromObject (new { result = Array.Empty () }); - - var var_list = new List (); - 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 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 (); - - 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 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 (); - - 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 RuntimeReady (SessionId sessionId, CancellationToken token) - { - var context = GetContext (sessionId); - if (Interlocked.CompareExchange (ref context.ready, new TaskCompletionSource (), 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 (); - - 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 (); - - 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 (); - - 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 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 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); - } - } - } -} diff --git a/src/Components/WebAssembly/DebugProxy/src/Program.cs b/src/Components/WebAssembly/DebugProxy/src/Program.cs deleted file mode 100644 index ce67276e6e..0000000000 --- a/src/Components/WebAssembly/DebugProxy/src/Program.cs +++ /dev/null @@ -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; - } - } - } -} diff --git a/src/Components/WebAssembly/DebugProxy/src/Startup.cs b/src/Components/WebAssembly/DebugProxy/src/Startup.cs deleted file mode 100644 index 9a80b2a252..0000000000 --- a/src/Components/WebAssembly/DebugProxy/src/Startup.cs +++ /dev/null @@ -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(); - var browserUri = new Uri(context.Request.Query["browser"]); - var ideSocket = await context.WebSockets.AcceptWebSocketAsync(); - await new MonoProxy(loggerFactory).Run(browserUri, ideSocket); - }); - }); - } - } -} diff --git a/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs b/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs index a8351af106..f6cc0ebe21 100644 --- a/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs +++ b/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Builder var processStartInfo = new ProcessStartInfo { FileName = muxerPath, - Arguments = $"exec \"{executablePath}\" --owner-pid {ownerPid}", + Arguments = $"exec \"{executablePath}\"", UseShellExecute = false, RedirectStandardOutput = true, }; @@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Builder var debugProxyPath = Path.Combine( Path.GetDirectoryName(assembly.Location), "BlazorDebugProxy", - "Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.dll"); + "BrowserDebugHost.dll"); if (!File.Exists(debugProxyPath)) { diff --git a/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj b/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj index 0b94d5c6e8..f77658c0e0 100644 --- a/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj +++ b/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj @@ -12,6 +12,9 @@ + + + @@ -22,27 +25,13 @@ - - - - - - + - - diff --git a/src/Components/WebAssembly/DebugProxy/src/TargetPickerUi.cs b/src/Components/WebAssembly/Server/src/TargetPickerUi.cs similarity index 91% rename from src/Components/WebAssembly/DebugProxy/src/TargetPickerUi.cs rename to src/Components/WebAssembly/Server/src/TargetPickerUi.cs index 249e2e5f37..e063ba8fef 100644 --- a/src/Components/WebAssembly/DebugProxy/src/TargetPickerUi.cs +++ b/src/Components/WebAssembly/Server/src/TargetPickerUi.cs @@ -12,7 +12,7 @@ using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy +namespace Microsoft.AspNetCore.Components.WebAssembly.Server { public class TargetPickerUi { @@ -23,11 +23,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy 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) @@ -37,7 +38,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy var request = context.Request; var targetApplicationUrl = request.Query["url"]; - var debuggerTabsListUrl = $"{_options.BrowserHost}/json"; + var debuggerTabsListUrl = $"{BrowserHost}/json"; IEnumerable availableTabs; try @@ -134,17 +135,17 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy private string GetDevToolsUrlWithProxy(HttpRequest request, BrowserTab tabToDebug) { - var underlyingV8Endpoint = tabToDebug.WebSocketDebuggerUrl; - var proxyEndpoint = GetProxyEndpoint(request, underlyingV8Endpoint); - var devToolsUrlAbsolute = new Uri(_options.BrowserHost + tabToDebug.DevtoolsFrontendUrl); - var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?{proxyEndpoint.Scheme}={proxyEndpoint.Authority}{proxyEndpoint.PathAndQuery}"; + var underlyingV8Endpoint = new Uri(tabToDebug.WebSocketDebuggerUrl); + var proxyEndpoint = new Uri(_debugProxyUrl); + var devToolsUrlAbsolute = new Uri(BrowserHost + tabToDebug.DevtoolsFrontendUrl); + var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?{underlyingV8Endpoint.Scheme}={proxyEndpoint.Authority}{underlyingV8Endpoint.PathAndQuery}"; return devToolsUrlWithProxy; } private string GetLaunchChromeInstructions(string targetApplicationUrl) { 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)) { @@ -170,7 +171,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy private string GetLaunchEdgeInstructions(string targetApplicationUrl) { 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)) { @@ -209,7 +210,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy private async Task> GetOpenedBrowserTabs() { 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(jsonResponse, JsonOptions); } diff --git a/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs index 6d51d4ff9b..25e80543b0 100644 --- a/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs +++ b/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Net; +using Microsoft.AspNetCore.Components.WebAssembly.Server; namespace Microsoft.AspNetCore.Builder { @@ -27,11 +28,12 @@ namespace Microsoft.AspNetCore.Builder 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) { case "/": + var targetPickerUi = new TargetPickerUi(debugProxyBaseUrl); + await targetPickerUi.Display(context); + break; case "/ws-proxy": context.Response.Redirect($"{debugProxyBaseUrl}{requestPath}{context.Request.QueryString}"); break;