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 index 933fcb9386..4ae030c63a 100644 --- a/src/Components/WebAssembly/DebugProxy/src/Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj +++ b/src/Components/WebAssembly/DebugProxy/src/Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs new file mode 100644 index 0000000000..441426299d --- /dev/null +++ b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsHelper.cs @@ -0,0 +1,256 @@ +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; + } + + public override int GetHashCode () + => sessionId?.GetHashCode () ?? 0; + + public override bool Equals (object obj) + => (obj is SessionId) ? ((SessionId) obj).sessionId == sessionId : false; + + 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 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 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 GetObjectProperties (int objectId, bool expandValueTypes) + => new MonoCommands ($"MONO.mono_wasm_get_object_properties({objectId}, { (expandValueTypes ? "true" : "false") })"); + + public static MonoCommands GetArrayValues (int objectId) + => new MonoCommands ($"MONO.mono_wasm_get_array_values({objectId})"); + + public static MonoCommands GetArrayValueExpanded (int objectId, int idx) + => new MonoCommands ($"MONO.mono_wasm_get_array_value_expanded({objectId}, {idx})"); + + 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, int 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})"); + } + + 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 (); + + int nextValueTypeId = 0; + public Dictionary ValueTypesCache = new Dictionary (); + public Dictionary LocalsCache = new Dictionary (); + + public DebugStore Store { + get { + if (store == null || !Source.Task.IsCompleted) + return null; + + return store; + } + } + + public void ClearState () + { + CallStack = null; + ValueTypesCache.Clear (); + LocalsCache.Clear (); + nextValueTypeId = 0; + } + + public int NextValueTypeId () => Interlocked.Increment (ref nextValueTypeId); + + } +} diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs index 42c7e33342..22d86b20f8 100644 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs +++ b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs @@ -11,103 +11,6 @@ using System.Collections.Generic; using Microsoft.Extensions.Logging; namespace WebAssembly.Net.Debugging { - internal struct SessionId { - public readonly string sessionId; - - public SessionId (string sessionId) - { - this.sessionId = sessionId; - } - - public override int GetHashCode () - => sessionId?.GetHashCode () ?? 0; - - public override bool Equals (object obj) - => (obj is SessionId) ? ((SessionId) obj).sessionId == sessionId : false; - - 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 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 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 - }); - } - } - } class DevToolsQueue { Task current_send; @@ -293,11 +196,12 @@ namespace WebAssembly.Net.Debugging { int id = Interlocked.Increment (ref next_cmd_id); var o = JObject.FromObject (new { - sessionId.sessionId, id, method, @params = args }); + if (sessionId.sessionId != null) + o["sessionId"] = sessionId.sessionId; var tcs = new TaskCompletionSource (); var msgId = new MessageId (sessionId.sessionId, id); @@ -317,10 +221,11 @@ namespace WebAssembly.Net.Debugging { void SendEventInternal (SessionId sessionId, string method, JObject args, CancellationToken token) { var o = JObject.FromObject (new { - sessionId.sessionId, method, @params = args }); + if (sessionId.sessionId != null) + o["sessionId"] = sessionId.sessionId; Send (this.ide, o, token); } diff --git a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs new file mode 100644 index 0000000000..fb7e776ed9 --- /dev/null +++ b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/EvaluateExpression.cs @@ -0,0 +1,182 @@ +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 index 0b1f707092..998e20eeba 100644 --- a/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs +++ b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs @@ -8,137 +8,17 @@ using System.IO; using System.Collections.Generic; using System.Net; using Microsoft.Extensions.Logging; +using Microsoft.CodeAnalysis; + namespace WebAssembly.Net.Debugging { - 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 GetObjectProperties (int objectId) - => new MonoCommands ($"MONO.mono_wasm_get_object_properties({objectId})"); - - public static MonoCommands GetArrayValues (int objectId) - => new MonoCommands ($"MONO.mono_wasm_get_array_values({objectId})"); - - 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, int 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})"); - } - - 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 DebugStore Store { - get { - if (store == null || !Source.Task.IsCompleted) - return null; - - return store; - } - } - } - internal class MonoProxy : DevToolsProxy { Dictionary contexts = new Dictionary (); public MonoProxy (ILoggerFactory loggerFactory) : base(loggerFactory) { } - ExecutionContext GetContext (SessionId sessionId) + internal ExecutionContext GetContext (SessionId sessionId) { if (contexts.TryGetValue (sessionId, out var context)) return context; @@ -221,11 +101,14 @@ namespace WebAssembly.Net.Debugging { protected override async Task AcceptCommand (MessageId id, string method, JObject args, CancellationToken token) { + if (!contexts.TryGetValue (id, out var context)) + return false; + switch (method) { case "Debugger.enable": { var resp = await SendCommand (id, method, args, token); - GetContext (id).DebuggerId = resp.Value ["debuggerId"]?.ToString (); + context.DebuggerId = resp.Value ["debuggerId"]?.ToString (); if (await IsRuntimeAlreadyReadyAlready (id, token)) await RuntimeReady (id, token); @@ -270,7 +153,6 @@ namespace WebAssembly.Net.Debugging { } case "Debugger.setBreakpointByUrl": { - var context = GetContext (id); var resp = await SendCommand (id, method, args, token); if (!resp.IsOk) { SendResponse (id, resp, token); @@ -312,6 +194,23 @@ namespace WebAssembly.Net.Debugging { return await Step (id, StepKind.Over, token); } + case "Debugger.evaluateOnCallFrame": { + var objId = args? ["callFrameId"]?.Value (); + if (objId.StartsWith ("dotnet:", StringComparison.Ordinal)) { + var parts = objId.Split (new char [] { ':' }); + if (parts.Length < 3) + return true; + switch (parts [1]) { + case "scope": { + await GetEvaluateOnCallFrame (id, int.Parse (parts [2]), args? ["expression"]?.Value (), token); + break; + } + } + return true; + } + return false; + } + case "Runtime.getProperties": { var objId = args? ["objectId"]?.Value (); if (objId.StartsWith ("dotnet:", StringComparison.Ordinal)) { @@ -324,14 +223,26 @@ namespace WebAssembly.Net.Debugging { break; } case "object": { - await GetDetails (id, MonoCommands.GetObjectProperties (int.Parse (parts[2])), token); + await GetDetails (id, MonoCommands.GetObjectProperties (int.Parse (parts[2]), expandValueTypes: false), token); break; } case "array": { - await GetDetails (id, MonoCommands.GetArrayValues (int.Parse (parts [2])), token); + await GetArrayDetails (id, objId, parts, token); + break; + } + case "valuetype": { + await GetDetailsForValueType (id, objId, + get_props_cmd_fn: () => { + if (parts.Length < 4) + return null; + + var containerObjId = int.Parse (parts[2]); + return MonoCommands.GetObjectProperties (containerObjId, expandValueTypes: true); + }, token); break; } } + return true; } break; @@ -341,6 +252,30 @@ namespace WebAssembly.Net.Debugging { return false; } + async Task GetArrayDetails (MessageId id, string objId, string[] objIdParts, CancellationToken token) + { + switch (objIdParts.Length) + { + case 3: { + await GetDetails (id, MonoCommands.GetArrayValues (int.Parse (objIdParts [2])), token); + break; + } + case 4: { + // This form of the id is being used only for valuetypes right now + await GetDetailsForValueType(id, objId, + get_props_cmd_fn: () => { + var arrayObjectId = int.Parse (objIdParts [2]); + var idx = int.Parse (objIdParts [3]); + return MonoCommands.GetArrayValueExpanded (arrayObjectId, idx); + }, token); + break; + } + default: + SendResponse (id, Result.Exception (new ArgumentException ($"Unknown objectId format for array: {objId}")), token); + break; + } + } + //static int frame_id=0; async Task OnBreakpointHit (SessionId sessionId, JObject args, CancellationToken token) { @@ -381,6 +316,7 @@ namespace WebAssembly.Net.Debugging { 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 (); @@ -411,11 +347,11 @@ namespace WebAssembly.Net.Debugging { 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)); + frames.Add (new Frame (method, location, frame_id-1)); callFrames.Add (new { functionName = method.Name, - callFrameId = $"dotnet:scope:{frame_id}", + callFrameId = $"dotnet:scope:{frame_id-1}", functionLocation = method.StartLocation.AsLocation (), location = location.AsLocation (), @@ -429,7 +365,7 @@ namespace WebAssembly.Net.Debugging { @type = "object", className = "Object", description = "Object", - objectId = $"dotnet:scope:{frame_id}", + objectId = $"dotnet:scope:{frame_id-1}", }, name = method.Name, startLocation = method.StartLocation.AsLocation (), @@ -437,7 +373,6 @@ namespace WebAssembly.Net.Debugging { }} }); - ++frame_id; context.CallStack = frames; } @@ -477,7 +412,7 @@ namespace WebAssembly.Net.Debugging { async Task OnResume (MessageId msd_id, CancellationToken token) { //discard managed frames - GetContext (msd_id).CallStack = null; + GetContext (msd_id).ClearState (); await Task.CompletedTask; } @@ -495,30 +430,20 @@ namespace WebAssembly.Net.Debugging { var ret_code = res.Value? ["result"]? ["value"]?.Value (); if (ret_code.HasValue && ret_code.Value == 0) { - context.CallStack = null; + context.ClearState (); await SendCommand (msg_id, "Debugger.stepOut", new JObject (), token); return false; - } + } SendResponse (msg_id, Result.Ok (new JObject ()), token); - context.CallStack = null; + context.ClearState (); await SendCommand (msg_id, "Debugger.resume", new JObject (), token); return true; } - static string FormatFieldName (string name) - { - if (name.Contains("k__BackingField", StringComparison.Ordinal)) { - return name.Replace("k__BackingField", "", StringComparison.Ordinal) - .Replace("<", "", StringComparison.Ordinal) - .Replace(">", "", StringComparison.Ordinal); - } - return name; - } - - async Task GetDetails(MessageId msg_id, MonoCommands cmd, CancellationToken token) + async Task GetDetails(MessageId msg_id, MonoCommands cmd, CancellationToken token, bool send_response = true) { var res = await SendMonoCommand(msg_id, cmd, token); @@ -529,43 +454,124 @@ namespace WebAssembly.Net.Debugging { } try { - var values = res.Value?["result"]?["value"]?.Values().ToArray() ?? Array.Empty(); - var var_list = new List(); + var var_list = res.Value?["result"]?["value"]?.Values().ToArray() ?? Array.Empty(); + if (var_list.Length > 0) + ExtractAndCacheValueTypes (GetContext (msg_id), var_list); - // Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously - // results in a "Memory access out of bounds", causing 'values' to be null, - // so skip returning variable values in that case. - for (int i = 0; i < values.Length; i+=2) - { - string fieldName = FormatFieldName ((string)values[i]["name"]); - var value = values [i + 1]? ["value"]; - if (((string)value ["description"]) == null) - value ["description"] = value ["value"]?.ToString (); + if (!send_response) + return; - var_list.Add(JObject.FromObject(new { - name = fieldName, - value - })); - - } var response = JObject.FromObject(new { result = var_list }); SendResponse(msg_id, Result.Ok(response), token); - } catch (Exception e) { + } catch (Exception e) when (send_response) { Log ("verbose", $"failed to parse {res.Value} - {e.Message}"); SendResponse(msg_id, Result.Exception(e), token); } } + 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 vars = scope.Method.GetLiveVarsAt (scope.Location.CliLocation.Offset); + //get_this + int [] var_ids = { }; + var res = await SendMonoCommand (msg_id, MonoCommands.GetScopeVariables (scope.Id, var_ids), token); + var values = res.Value? ["result"]? ["value"]?.Values ().ToArray (); + thisValue = values.FirstOrDefault (v => v ["name"].Value () == "this"); + + if (!only_search_on_this) { + if (thisValue != null && expression == "this") { + return thisValue; + } + //search in locals + var var_id = vars.SingleOrDefault (v => v.Name == expression); + if (var_id != null) { + res = await SendMonoCommand (msg_id, MonoCommands.GetScopeVariables (scope.Id, new int [] { var_id.Index }), token); + values = res.Value? ["result"]? ["value"]?.Values ().ToArray (); + return values [0]; + } + } + + //search in scope + if (thisValue != null) { + var objectId = thisValue ["value"] ["objectId"].Value (); + var parts = objectId.Split (new char [] { ':' }); + res = await SendMonoCommand (msg_id, MonoCommands.GetObjectProperties (int.Parse (parts [2]), expandValueTypes: false), token); + values = res.Value? ["result"]? ["value"]?.Values ().ToArray (); + var foundValue = 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 GetEvaluateOnCallFrame (MessageId msg_id, int scope_id, string expression, CancellationToken token) + { + try { + var context = GetContext (msg_id); + if (context.CallStack == null) + return; + + var varValue = await TryGetVariableValue (msg_id, scope_id, expression, false, token); + + if (varValue != null) { + varValue ["value"] ["description"] = varValue ["value"] ["className"]; + SendResponse (msg_id, Result.OkFromObject (new { + result = varValue ["value"] + }), token); + return; + } + + string retValue = await EvaluateExpression.CompileAndRunTheExpression (this, msg_id, scope_id, expression, token); + SendResponse (msg_id, Result.OkFromObject (new { + result = new { + value = retValue + } + }), token); + } catch (Exception e) { + logger.LogTrace (e.Message, expression); + SendResponse (msg_id, Result.OkFromObject (new {}), token); + } + } + async Task GetScopeProperties (MessageId msg_id, int scope_id, CancellationToken token) { try { - var scope = GetContext (msg_id).CallStack.FirstOrDefault (s => s.Id == scope_id); + var ctx = GetContext (msg_id); + var scope = ctx.CallStack.FirstOrDefault (s => s.Id == scope_id); + if (scope == null) { + SendResponse (msg_id, + Result.Err (JObject.FromObject (new { message = $"Could not find scope with id #{scope_id}" })), + token); + return; + } var vars = scope.Method.GetLiveVarsAt (scope.Location.CliLocation.Offset); var var_ids = vars.Select (v => v.Index).ToArray (); @@ -584,39 +590,17 @@ namespace WebAssembly.Net.Debugging { return; } + ExtractAndCacheValueTypes (ctx, values); + var var_list = new List (); int i = 0; - // Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously - // results in a "Memory access out of bounds", causing 'values' to be null, - // so skip returning variable values in that case. - while (i < vars.Length && i < values.Length) { - var value = values [i] ["value"]; - if (((string)value ["description"]) == null) - value ["description"] = value ["value"]?.ToString (); - - var_list.Add (new { - name = vars [i].Name, - value - }); - i++; + for (; i < vars.Length && i < values.Length; i ++) { + ctx.LocalsCache[vars [i].Name] = values [i]; + var_list.Add (new { name = vars [i].Name, value = values [i]["value"] }); } - //Async methods are special in the way that local variables can be lifted to generated class fields - //value of "this" comes here either - while (i < values.Length) { - String name = values [i] ["name"].ToString (); - - if (name.IndexOf (">", StringComparison.Ordinal) > 0) - name = name.Substring (1, name.IndexOf (">", StringComparison.Ordinal) - 1); - - var value = values [i + 1] ["value"]; - if (((string)value ["description"]) == null) - value ["description"] = value ["value"]?.ToString (); - - var_list.Add (new { - name, - value - }); - i = i + 2; + for (; i < values.Length; i ++) { + ctx.LocalsCache[values [i]["name"].ToString()] = values [i]["value"]; + var_list.Add (values [i]); } SendResponse (msg_id, Result.OkFromObject (new { result = var_list }), token); @@ -626,6 +610,68 @@ namespace WebAssembly.Net.Debugging { } } + IEnumerable ExtractAndCacheValueTypes (ExecutionContext ctx, IEnumerable var_list) + { + foreach (var jo in var_list) { + var value = jo["value"]?.Value (); + if (value ["type"]?.Value () != "object") + continue; + + if (!(value ["isValueType"]?.Value () ?? false) || // not a valuetype + !(value ["expanded"]?.Value () ?? false)) // not expanded + continue; + + // Expanded ValueType + var members = value ["members"]?.Values().ToArray() ?? Array.Empty(); + var objectId = value ["objectId"]?.Value () ?? $"dotnet:valuetype:{ctx.NextValueTypeId ()}"; + + value ["objectId"] = objectId; + + ExtractAndCacheValueTypes (ctx, members); + + ctx.ValueTypesCache [objectId] = JArray.FromObject (members); + value.Remove ("members"); + } + + return var_list; + } + + async Task GetDetailsForValueType (MessageId msg_id, string object_id, Func get_props_cmd_fn, CancellationToken token) + { + var ctx = GetContext (msg_id); + + if (!ctx.ValueTypesCache.ContainsKey (object_id)) { + var cmd = get_props_cmd_fn (); + if (cmd == null) { + SendResponse (msg_id, Result.Exception (new ArgumentException ( + "Could not find a cached value for {object_id}, and cant' expand it.")), + token); + + return false; + } else { + await GetDetails (msg_id, cmd, token, send_response: false); + } + } + + if (ctx.ValueTypesCache.TryGetValue (object_id, out var var_list)) { + var response = JObject.FromObject(new + { + result = var_list + }); + + SendResponse(msg_id, Result.Ok(response), token); + return true; + } else { + var response = JObject.FromObject(new + { + result = $"Unable to get details for {object_id}" + }); + + SendResponse(msg_id, Result.Err(response), token); + return false; + } + } + async Task SetMonoBreakpoint (SessionId sessionId, BreakpointRequest req, SourceLocation location, CancellationToken token) { var bp = new Breakpoint (req.Id, location, BreakpointState.Pending);