diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DebugStore.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DebugStore.cs index 1b9568de804a..f633fe786d59 100644 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DebugStore.cs +++ b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DebugStore.cs @@ -375,14 +375,14 @@ class AssemblyInfo { readonly List sources = new List(); internal string Url { get; } - public AssemblyInfo (string url, byte[] assembly, byte[] pdb) + public AssemblyInfo (IAssemblyResolver resolver, string url, byte[] assembly, byte[] pdb) { this.id = Interlocked.Increment (ref next_id); try { Url = url; ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/); - + rp.AssemblyResolver = resolver; // set ReadSymbols = true unconditionally in case there // is an embedded pdb then handle ArgumentException // and assume that if pdb == null that is the cause @@ -391,7 +391,6 @@ public AssemblyInfo (string url, byte[] assembly, byte[] pdb) 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) { @@ -405,6 +404,7 @@ public AssemblyInfo (string url, byte[] assembly, byte[] pdb) if (this.image == null) { ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/); + rp.AssemblyResolver = resolver; if (pdb != null) { rp.ReadSymbols = true; rp.SymbolReaderProvider = new PdbReaderProvider (); @@ -412,7 +412,6 @@ public AssemblyInfo (string url, byte[] assembly, byte[] pdb) } rp.ReadingMode = ReadingMode.Immediate; - rp.InMemory = true; this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp); } @@ -500,19 +499,10 @@ private Uri GetSourceLinkUrl (string document) 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 Dictionary TypesByName => this.typesByName; public int Id => id; public string Name => image.Name; @@ -588,13 +578,13 @@ async Task GetDataAsync (Uri uri, CancellationToken token) try { if (uri.IsFile && File.Exists (uri.LocalPath)) { using (var file = File.Open (SourceUri.LocalPath, FileMode.Open)) { - await file.CopyToAsync (mem, token); + await file.CopyToAsync (mem, token).ConfigureAwait (false); 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); + await stream.CopyToAsync (mem, token).ConfigureAwait (false); mem.Position = 0; } } @@ -641,18 +631,12 @@ byte[] ComputePdbHash (Stream sourceStream) 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; + foreach (var url in new [] { SourceUri, SourceLinkUri }) { + var mem = await GetDataAsync (url, token).ConfigureAwait (false); + if (mem != null && (!checkHash || CheckPdbHash (ComputePdbHash (mem)))) { + mem.Position = 0; + return mem; + } } return MemoryStream.Null; @@ -718,11 +702,12 @@ static bool MatchPdb (string asm, string pdb) } } + var resolver = new DefaultAssemblyResolver (); foreach (var step in steps) { AssemblyInfo assembly = null; try { - var bytes = await step.Data; - assembly = new AssemblyInfo (step.Url, bytes [0], bytes [1]); + var bytes = await step.Data.ConfigureAwait (false); + assembly = new AssemblyInfo (resolver, step.Url, bytes [0], bytes [1]); } catch (Exception e) { logger.LogDebug ($"Failed to load {step.Url} ({e.Message})"); } diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs index 871c10de9b01..a615540e4502 100644 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs +++ b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Threading; @@ -190,8 +191,11 @@ public static MonoCommands ClearAllBreakpoints () 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 GetScopeVariables (int scopeId, params VarInfo[] vars) + { + var var_ids = vars.Select (v => new { index = v.Index, name = v.Name }).ToArray (); + return new MonoCommands ($"MONO.mono_wasm_get_variables({scopeId}, {JsonConvert.SerializeObject (var_ids)})"); + } public static MonoCommands SetBreakpoint (string assemblyName, uint methodToken, int ilOffset) => new MonoCommands ($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})"); @@ -204,6 +208,9 @@ public static MonoCommands ReleaseObject (DotnetObjectId objectId) public static MonoCommands CallFunctionOn (JToken args) => new MonoCommands ($"MONO.mono_wasm_call_function_on ({args.ToString ()})"); + + public static MonoCommands Resume () + => new MonoCommands ($"MONO.mono_wasm_debugger_resume ()"); } internal enum MonoErrorCodes { @@ -274,6 +281,7 @@ internal class ExecutionContext { public List CallStack { get; set; } + public string[] LoadedFiles { get; set; } internal DebugStore store; public TaskCompletionSource Source { get; } = new TaskCompletionSource (); diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs index fb7e776ed902..7bcf6f811746 100644 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs +++ b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs @@ -78,42 +78,58 @@ public async Task ReplaceVars (SyntaxTree syntaxTree, MonoProxy prox 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 ())); + values.Add (ConvertJSToCSharpType (value ["value"])); var updatedMethod = method.AddParameterListParameters ( SyntaxFactory.Parameter ( SyntaxFactory.Identifier (var.Identifier.Text)) - .WithType (SyntaxFactory.ParseTypeName (GetTypeFullName(value["value"]["type"].ToString())))); + .WithType (SyntaxFactory.ParseTypeName (GetTypeFullName(value["value"])))); root = root.ReplaceNode (method, updatedMethod); } syntaxTree = syntaxTree.WithRootAndOptions (root, syntaxTree.Options); return syntaxTree; } - private object ConvertJSToCSharpType (string v, string type) + private object ConvertJSToCSharpType (JToken variable) { + var value = variable["value"]; + var type = variable["type"].Value(); + var subType = variable["subtype"]?.Value(); + switch (type) { - case "number": - return Convert.ChangeType (v, typeof (int)); - case "string": - return v; + case "string": + return value?.Value (); + case "number": + return value?.Value (); + case "boolean": + return value?.Value (); + case "object": + if (subType == "null") + return null; + break; } - throw new Exception ($"Evaluate of this datatype {type} not implemented yet"); } - private string GetTypeFullName (string type) + private string GetTypeFullName (JToken variable) { + var type = variable["type"].ToString (); + var subType = variable["subtype"]?.Value(); + object value = ConvertJSToCSharpType (variable); + switch (type) { - case "number": - return typeof (int).FullName; - case "string": - return typeof (string).FullName; + case "object": { + if (subType == "null") + return variable["className"].Value(); + break; + } + default: + return value.GetType ().FullName; } - throw new Exception ($"Evaluate of this datatype {type} not implemented yet"); } } + static SyntaxNode GetExpressionFromSyntaxTree (SyntaxTree syntaxTree) { CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot (); @@ -126,6 +142,7 @@ static SyntaxNode GetExpressionFromSyntaxTree (SyntaxTree syntaxTree) 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 (); @@ -172,7 +189,6 @@ public string Evaluate() BindingFlags.Default | BindingFlags.InvokeMethod, null, obj, - //new object [] { 10 } findVarNMethodCall.values.ToArray ()); retString = ret.ToString (); } diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs index 1b26dd93fd40..6f8532b6efbd 100644 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs +++ b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs @@ -17,7 +17,7 @@ internal class MonoProxy : DevToolsProxy { HashSet sessions = new HashSet (); Dictionary contexts = new Dictionary (); - public MonoProxy (ILoggerFactory loggerFactory, bool hideWebDriver = true) : base(loggerFactory) { this.hideWebDriver = hideWebDriver; } + public MonoProxy (ILoggerFactory loggerFactory, bool hideWebDriver = true) : base(loggerFactory) { hideWebDriver = true; } readonly bool hideWebDriver; @@ -45,8 +45,24 @@ protected override async Task AcceptEvent (SessionId sessionId, string met 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") + var a = args ["args"]; + if (a? [0]? ["value"]?.ToString () == MonoConstants.RUNTIME_IS_READY && + a? [1]? ["value"]?.ToString () == "fe00e07a-5519-4dfe-b35a-f867dbaf2e28") { + if (a.Count () > 2) { + try { + // The optional 3rd argument is the stringified assembly + // list so that we don't have to make more round trips + var context = GetContext (sessionId); + var loaded = a? [2]? ["value"]?.ToString (); + if (loaded != null) + context.LoadedFiles = JToken.Parse (loaded).ToObject (); + } catch (InvalidCastException ice) { + Log ("verbose", ice.ToString ()); + } + } await RuntimeReady (sessionId, token); + } + } break; } @@ -106,12 +122,13 @@ protected override async Task AcceptEvent (SessionId sessionId, string met async Task IsRuntimeAlreadyReadyAlready (SessionId sessionId, CancellationToken token) { + if (contexts.TryGetValue (sessionId, out var context) && context.IsRuntimeReady) + return true; + 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 @@ -269,7 +286,7 @@ protected override async Task AcceptCommand (MessageId id, string method, } // Protocol extensions - case "Dotnet-test.setBreakpointByMethod": { + case "DotnetDebugger.getMethodLocation": { Console.WriteLine ("set-breakpoint-by-method: " + id + " " + args); var store = await RuntimeReady (id, token); @@ -300,31 +317,21 @@ protected override async Task AcceptCommand (MessageId id, string method, var methodInfo = type.Methods.FirstOrDefault (m => m.Name == methodName); if (methodInfo == null) { - SendResponse (id, Result.Err ($"Method '{typeName}:{methodName}' not found."), token); - return true; + // Maybe this is an async method, in which case the debug info is attached + // to the async method implementation, in class named: + // `{type_name}/::MoveNext` + methodInfo = assembly.TypesByName.Values.SingleOrDefault (t => t.FullName.StartsWith ($"{typeName}/<{methodName}>")) + ?.Methods.FirstOrDefault (mi => mi.Name == "MoveNext"); } - 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 (); + if (methodInfo == null) { + SendResponse (id, Result.Err ($"Method '{typeName}:{methodName}' not found."), token); + return true; } - var resolvedLocation = new { - breakpointId = bpid, - location = loc.AsLocation () - }; - - SendEvent (id, "Debugger.breakpointResolved", JObject.FromObject (resolvedLocation), token); - + var src_url = methodInfo.Assembly.Sources.Single (sf => sf.SourceId == methodInfo.SourceId).Url; SendResponse (id, Result.OkFromObject (new { - result = new { breakpointId = bpid, locations = new object [] { loc.AsLocation () }} + result = new { line = methodInfo.StartLocation.Line, column = methodInfo.StartLocation.Column, url = src_url } }), token); return true; @@ -333,25 +340,20 @@ protected override async Task AcceptCommand (MessageId id, string method, 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); + SendResponse (id, + Result.Exception (new ArgumentException ( + $"Runtime.callFunctionOn not supported with scope ({objectId}).")), + token); return true; } - var returnByValue = args ["returnByValue"]?.Value () ?? false; var res = await SendMonoCommand (id, MonoCommands.CallFunctionOn (args), token); + var res_value_type = res.Value? ["result"]? ["value"]?.Type; - if (!returnByValue && - DotnetObjectId.TryParse (res.Value?["result"]?["value"]?["objectId"], out var resultObjectId) && - resultObjectId.Scheme == "cfo_res") + if (res.IsOk && res_value_type == JTokenType.Object || res_value_type == JTokenType.Object) 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; } @@ -374,7 +376,7 @@ async Task RuntimeGetProperties (MessageId id, DotnetObjectId objectId, 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 (@"\""", "\"")) + result = JArray.Parse (value_json_str) }); } else { res = Result.OkFromObject (new { result = new {} }); @@ -522,11 +524,16 @@ async Task OnDefaultContext (SessionId sessionId, ExecutionContext context, Canc await RuntimeReady (sessionId, token); } - async Task OnResume (MessageId msd_id, CancellationToken token) + async Task OnResume (MessageId msg_id, CancellationToken token) { + var ctx = GetContext (msg_id); + if (ctx.CallStack != null) { + // Stopped on managed code + await SendMonoCommand (msg_id, MonoCommands.Resume (), token); + } + //discard managed frames - GetContext (msd_id).ClearState (); - await Task.CompletedTask; + GetContext (msg_id).ClearState (); } async Task Step (MessageId msg_id, StepKind kind, CancellationToken token) @@ -579,7 +586,7 @@ internal async Task TryGetVariableValue (MessageId msg_id, int scope_id, 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 res = await SendMonoCommand (msg_id, MonoCommands.GetScopeVariables (scope.Id, live_vars), token); var scope_values = res.Value? ["result"]? ["value"]?.Values ()?.ToArray (); thisValue = scope_values?.FirstOrDefault (v => v ["name"]?.Value () == "this"); @@ -647,9 +654,7 @@ async Task GetScopeProperties (MessageId msg_id, int scope_id, Cancellat 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 var_ids = scope.Method.GetLiveVarsAt (scope.Location.CliLocation.Offset); 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) @@ -658,27 +663,13 @@ async Task GetScopeProperties (MessageId msg_id, int scope_id, Cancellat var values = res.Value? ["result"]? ["value"]?.Values ().ToArray (); - if(values == null) + if(values == null || values.Length == 0) 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]); - } + foreach (var value in values) + ctx.LocalsCache [value ["name"]?.Value ()] = value; - return Result.OkFromObject (new { result = var_list }); + return Result.OkFromObject (new { result = values }); } catch (Exception exception) { Log ("verbose", $"Error resolving scope properties {exception.Message}"); return Result.Exception (exception); @@ -712,11 +703,14 @@ async Task LoadStore (SessionId sessionId, CancellationToken token) 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 (); + var loaded_files = context.LoadedFiles; + + if (loaded_files == null) { + var loaded = await SendMonoCommand (sessionId, MonoCommands.GetLoadedFiles (), token); + loaded_files = loaded.Value? ["result"]? ["value"]?.ToObject (); + } - await foreach (var source in context.store.Load(sessionId, the_pdbs, token).WithCancellation (token)) { + await foreach (var source in context.store.Load(sessionId, loaded_files, token).WithCancellation (token)) { var scriptSource = JObject.FromObject (source.ToScriptSource (context.Id, context.AuxData)); Log ("verbose", $"\tsending {source.Url} {context.Id} {sessionId.sessionId}");