diff --git a/src/mono/mono/component/debugger-agent.c b/src/mono/mono/component/debugger-agent.c index f9712c5b77e380..d558691594751e 100644 --- a/src/mono/mono/component/debugger-agent.c +++ b/src/mono/mono/component/debugger-agent.c @@ -7012,6 +7012,29 @@ vm_commands (int command, int id, guint8 *p, guint8 *end, Buffer *buf) buffer_add_assemblyid (buf, mono_get_root_domain (), assembly); break; } + case MDBGPROT_CMD_GET_MODULE_BY_GUID: { + int len = 0; + uint8_t* guid = m_dbgprot_decode_byte_array (p, &p, end, &len); + MonoAssembly *assembly = NULL; + GPtrArray *assemblies = mono_alc_get_all_loaded_assemblies (); + for (int i = 0; i < assemblies->len; ++i) { + MonoAssembly *assemblyOnALC = (MonoAssembly*)g_ptr_array_index (assemblies, i); + if (!memcmp(assemblyOnALC->image->heap_guid.data, guid, len)) { + assembly = assemblyOnALC; + break; + } + } + g_ptr_array_free (assemblies, TRUE); + if (!assembly) { + PRINT_DEBUG_MSG (1, "Could not resolve guid\n"); + g_free (guid); + buffer_add_int (buf, -1); + break; + } + g_free (guid); + buffer_add_moduleid (buf, mono_get_root_domain (), assembly->image); + break; + } default: return ERR_NOT_IMPLEMENTED; } diff --git a/src/mono/mono/component/debugger-protocol.h b/src/mono/mono/component/debugger-protocol.h index 6bce7c5ff2ec1e..2aa52c2a857ffe 100644 --- a/src/mono/mono/component/debugger-protocol.h +++ b/src/mono/mono/component/debugger-protocol.h @@ -35,7 +35,8 @@ typedef enum { MDBGPROT_CMD_VM_STOP_BUFFERING = 15, MDBGPROT_CMD_VM_READ_MEMORY = 16, MDBGPROT_CMD_VM_WRITE_MEMORY = 17, - MDBGPROT_CMD_GET_ASSEMBLY_BY_NAME = 18 + MDBGPROT_CMD_GET_ASSEMBLY_BY_NAME = 18, + MDBGPROT_CMD_GET_MODULE_BY_GUID = 19 } MdbgProtCmdVM; typedef enum { @@ -150,7 +151,7 @@ typedef enum { typedef enum { MDBGPROT_CMD_MODULE_GET_INFO = 1, - MDBGPROT_CMD_MODULE_APPLY_CHANGES = 2, + MDBGPROT_CMD_MODULE_APPLY_CHANGES = 2 } MdbgProtCmdModule; typedef enum { diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 36d6289bf241c8..5fc72c491c3b58 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -499,6 +499,14 @@ protected override async Task AcceptCommand(MessageId id, string method, J } // Protocol extensions + case "DotnetDebugger.applyUpdates": + { + if (await ApplyUpdates(id, args, token)) + SendResponse(id, Result.OkFromObject(new { }), token); + else + SendResponse(id, Result.Err("ApplyUpdate failed."), token); + return true; + } case "DotnetDebugger.addSymbolServerUrl": { string url = args["url"]?.Value(); @@ -593,6 +601,19 @@ protected override async Task AcceptCommand(MessageId id, string method, J return false; } + + private async Task ApplyUpdates(MessageId id, JObject args, CancellationToken token) + { + var context = GetContext(id); + string moduleGUID = args["moduleGUID"]?.Value(); + string dmeta = args["dmeta"]?.Value(); + string dil = args["dil"]?.Value(); + string dpdb = args["dpdb"]?.Value(); + var moduleId = await context.SdbAgent.GetModuleId(moduleGUID, token); + var applyUpdates = await context.SdbAgent.ApplyUpdates(moduleId, dmeta, dil, dpdb, token); + return applyUpdates; + } + private void SetJustMyCode(MessageId id, JObject args, CancellationToken token) { var isEnabled = args["enabled"]?.Value(); diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index 3b1dc557259b68..1d62e94f556f1b 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -139,7 +139,8 @@ internal enum CmdVM { StopBuffering = 15, VmReadMemory = 16, VmWriteMemory = 17, - GetAssemblyByName = 18 + GetAssemblyByName = 18, + GetModuleByGUID = 19 } internal enum CmdFrame { @@ -483,8 +484,7 @@ public MonoBinaryWriter() : base(new MemoryStream(20)) {} public override void Write(string val) { var bytes = Encoding.UTF8.GetBytes(val); - Write(bytes.Length); - Write(bytes); + WriteByteArray(bytes); } public override void Write(long val) => WriteBigEndian(val); @@ -520,6 +520,13 @@ public void WriteObj(DotnetObjectId objectId, MonoSDBHelper SdbHelper) Write(SdbHelper.valueTypes[objectId.Value].valueTypeBuffer); } } + + public void WriteByteArray(byte[] bytes) + { + Write(bytes.Length); + Write(bytes); + } + public async Task WriteConst(LiteralExpressionSyntax constValue, MonoSDBHelper SdbHelper, CancellationToken token) { switch (constValue.Kind()) @@ -996,6 +1003,16 @@ public async Task GetAssemblyId(string asm_name, CancellationToken token) return retDebuggerCmdReader.ReadInt32(); } + public async Task GetModuleId(string moduleGuid, CancellationToken token) + { + using var commandParamsWriter = new MonoBinaryWriter(); + var guidArray = Convert.FromBase64String(moduleGuid); + commandParamsWriter.WriteByteArray(guidArray); + + using var retDebuggerCmdReader = await SendDebuggerAgentCommand(CmdVM.GetModuleByGUID, commandParamsWriter, token); + return retDebuggerCmdReader.ReadInt32(); + } + public async Task GetAssemblyNameFromModule(int moduleId, CancellationToken token) { using var command_params_writer = new MonoBinaryWriter(); @@ -2665,6 +2682,39 @@ public async Task SetVariableValue(int thread_id, int frame_id, int varId, return false; return true; } + + public async Task CreateByteArray(string diff, CancellationToken token) + { + var diffArr = Convert.FromBase64String(diff); + using var commandParamsWriter = new MonoBinaryWriter(); + using var retDebuggerCmdReader = await SendDebuggerAgentCommand(CmdAppDomain.GetRootDomain, commandParamsWriter, token); + var root = retDebuggerCmdReader.ReadInt32(); + + commandParamsWriter.Write(root); + commandParamsWriter.WriteByteArray(diffArr); + using var arrayDebuggerCmdReader = await SendDebuggerAgentCommand(CmdAppDomain.CreateByteArray, commandParamsWriter, token); + return arrayDebuggerCmdReader.ReadInt32(); + } + + public async Task ApplyUpdates(int moduleId, string dmeta, string dil, string dpdb, CancellationToken token) + { + int dpdbId = -1; + var dmetaId = await CreateByteArray(dmeta, token); + var dilId = await CreateByteArray(dil, token); + if (dpdb != null) + dpdbId = await CreateByteArray(dpdb, token); + + using var commandParamsWriter = new MonoBinaryWriter(); + commandParamsWriter.Write(moduleId); + commandParamsWriter.Write(dmetaId); + commandParamsWriter.Write(dilId); + if (dpdbId != -1) + commandParamsWriter.Write(dpdbId); + else + commandParamsWriter.Write((byte)ValueTypeId.Null); + await SendDebuggerAgentCommand(CmdModule.ApplyChanges, commandParamsWriter, token); + return true; + } } internal static class HelperExtensions diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs index b8c58a27f1e5fb..3dc2bc246364c9 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs @@ -707,7 +707,190 @@ await SendCommandAndCheck(null, "Debugger.resume", 8, "VisibleMethodDebuggerBreak"); } + + [Fact] + public async Task DebugHotReloadMethodChangedUserBreakUsingSDB() + { + string asm_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.dll"); + string pdb_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.pdb"); + string asm_file_hot_reload = Path.Combine(DebuggerTestAppPath, "../wasm/ApplyUpdateReferencedAssembly.dll"); + + var pause_location = await LoadAssemblyAndTestHotReloadUsingSDBWithoutChanges( + asm_file, pdb_file, "MethodBody1", "StaticMethod1"); + + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + CheckNumber(locals, "a", 10); + pause_location = await LoadAssemblyAndTestHotReloadUsingSDB( + asm_file_hot_reload, "MethodBody1", "StaticMethod1", 1); + + JToken top_frame = pause_location["callFrames"]?[0]; + AssertEqual("StaticMethod1", top_frame?["functionName"]?.Value(), top_frame?.ToString()); + CheckLocation("dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 12, 16, scripts, top_frame["location"]); + + locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + CheckNumber(locals, "b", 15); + pause_location = await LoadAssemblyAndTestHotReloadUsingSDB( + asm_file_hot_reload, "MethodBody1", "StaticMethod1", 2); + + top_frame = pause_location["callFrames"]?[0]; + AssertEqual("StaticMethod1", top_frame?["functionName"]?.Value(), top_frame?.ToString()); + CheckLocation("dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 12, 12, scripts, top_frame["location"]); + + locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + await CheckBool(locals, "c", true); + } + + [Fact] + public async Task DebugHotReloadMethodUnchangedUsingSDB() + { + string asm_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.dll"); + string pdb_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.pdb"); + string asm_file_hot_reload = Path.Combine(DebuggerTestAppPath, "../wasm/ApplyUpdateReferencedAssembly.dll"); + + var pause_location = await LoadAssemblyAndTestHotReloadUsingSDBWithoutChanges( + asm_file, pdb_file, "MethodBody2", "StaticMethod1"); + + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + CheckNumber(locals, "a", 10); + pause_location = await LoadAssemblyAndTestHotReloadUsingSDB( + asm_file_hot_reload, "MethodBody2", "StaticMethod1", 1); + + JToken top_frame = pause_location["callFrames"]?[0]; + AssertEqual("StaticMethod1", top_frame?["functionName"]?.Value(), top_frame?.ToString()); + CheckLocation("dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 21, 12, scripts, top_frame["location"]); + + locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + CheckNumber(locals, "a", 10); + pause_location = await LoadAssemblyAndTestHotReloadUsingSDB( + asm_file_hot_reload, "MethodBody2", "StaticMethod1", 2); + + top_frame = pause_location["callFrames"]?[0]; + AssertEqual("StaticMethod1", top_frame?["functionName"]?.Value(), top_frame?.ToString()); + CheckLocation("dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 21, 12, scripts, top_frame["location"]); + } + + [Fact] + public async Task DebugHotReloadMethodAddBreakpointUsingSDB() + { + string asm_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.dll"); + string pdb_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.pdb"); + string asm_file_hot_reload = Path.Combine(DebuggerTestAppPath, "../wasm/ApplyUpdateReferencedAssembly.dll"); + int line = 30; + await SetBreakpoint(".*/MethodBody1.cs$", line, 12, use_regex: true); + var pause_location = await LoadAssemblyAndTestHotReloadUsingSDBWithoutChanges( + asm_file, pdb_file, "MethodBody3", "StaticMethod3"); + + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + CheckNumber(locals, "a", 10); + + //apply first update + pause_location = await LoadAssemblyAndTestHotReloadUsingSDB( + asm_file_hot_reload, "MethodBody3", "StaticMethod3", 1); + + JToken top_frame = pause_location["callFrames"]?[0]; + AssertEqual("StaticMethod3", top_frame?["functionName"]?.Value(), top_frame?.ToString()); + CheckLocation("dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 30, 12, scripts, top_frame["location"]); + + locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + CheckNumber(locals, "b", 15); + + //apply second update + pause_location = await LoadAssemblyAndTestHotReloadUsingSDB( + asm_file_hot_reload, "MethodBody3", "StaticMethod3", 2); + + top_frame = pause_location["callFrames"]?[0]; + AssertEqual("StaticMethod3", top_frame?["functionName"]?.Value(), top_frame?.ToString()); + CheckLocation("dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 30, 12, scripts, top_frame["location"]); + + locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + await CheckBool(locals, "c", true); + + await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 31, 12, "StaticMethod3", + locals_fn: async (locals) => + { + CheckNumber(locals, "d", 10); + await Task.CompletedTask; + } + ); + await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 32, 12, "StaticMethod3", + locals_fn: async (locals) => + { + CheckNumber(locals, "d", 10); + CheckNumber(locals, "e", 20); + await Task.CompletedTask; + } + ); + await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 33, 8, "StaticMethod3", + locals_fn: async (locals) => + { + CheckNumber(locals, "d", 10); + CheckNumber(locals, "e", 20); + CheckNumber(locals, "f", 50); + await Task.CompletedTask; + } + ); + } + + + [Fact] + public async Task DebugHotReloadMethodEmptyUsingSDB() + { + string asm_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.dll"); + string pdb_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.pdb"); + string asm_file_hot_reload = Path.Combine(DebuggerTestAppPath, "../wasm/ApplyUpdateReferencedAssembly.dll"); + + int line = 38; + await SetBreakpoint(".*/MethodBody1.cs$", line, 0, use_regex: true); + var pause_location = await LoadAssemblyAndTestHotReloadUsingSDBWithoutChanges( + asm_file, pdb_file, "MethodBody4", "StaticMethod4"); + + //apply first update + pause_location = await LoadAssemblyAndTestHotReloadUsingSDB( + asm_file_hot_reload, "MethodBody4", "StaticMethod4", 1); + + await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 39, 12, "StaticMethod4", + locals_fn: async (locals) => + { + CheckNumber(locals, "a", 10); + await Task.CompletedTask; + } + ); + await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 40, 12, "StaticMethod4", + locals_fn: async (locals) => + { + CheckNumber(locals, "a", 10); + CheckNumber(locals, "b", 20); + await Task.CompletedTask; + } + ); + await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 41, 12, "StaticMethod4", + locals_fn: async (locals) => + { + CheckNumber(locals, "a", 10); + CheckNumber(locals, "b", 20); + await Task.CompletedTask; + } + ); + await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 42, 12, "StaticMethod4", + locals_fn: async (locals) => + { + CheckNumber(locals, "a", 10); + CheckNumber(locals, "b", 20); + await Task.CompletedTask; + } + ); + await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 43, 8, "StaticMethod4", + locals_fn: async (locals) => + { + CheckNumber(locals, "a", 10); + CheckNumber(locals, "b", 20); + await Task.CompletedTask; + } + ); + //pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", "dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 38, 8, "StaticMethod4"); + } + [Theory] [InlineData(false, "RunStepThrough")] [InlineData(true, "RunStepThrough")] diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs index d49a2331e2a11e..cf4eade5a81936 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs @@ -1189,9 +1189,72 @@ internal async Task LoadAssemblyDynamicallyALCAndRunMethod(string asm_f return await insp.WaitFor(Inspector.PAUSE); } + internal async Task LoadAssemblyAndTestHotReloadUsingSDBWithoutChanges(string asm_file, string pdb_file, string class_name, string method_name) + { + byte[] bytes = File.ReadAllBytes(asm_file); + string asm_base64 = Convert.ToBase64String(bytes); + bytes = File.ReadAllBytes(pdb_file); + string pdb_base64 = Convert.ToBase64String(bytes); + + string expression = $"let asm_b64 = '{asm_base64}'; let pdb_b64 = '{pdb_base64}';"; + expression = $"{{ {expression} invoke_static_method('[debugger-test] TestHotReloadUsingSDB:LoadLazyHotReload', asm_b64, pdb_b64); }}"; + var load_assemblies = JObject.FromObject(new + { + expression + }); + + Result load_assemblies_res = await cli.SendCommand("Runtime.evaluate", load_assemblies, token); + + Thread.Sleep(1000); + var run_method = JObject.FromObject(new + { + expression = "window.setTimeout(function() { invoke_static_method('[debugger-test] TestHotReloadUsingSDB:RunMethod', '" + class_name + "', '" + method_name + "'); }, 1);" + }); + + await cli.SendCommand("Runtime.evaluate", run_method, token); + return await insp.WaitFor(Inspector.PAUSE); + } + + internal async Task LoadAssemblyAndTestHotReloadUsingSDB(string asm_file_hot_reload, string class_name, string method_name, int id) + { + await cli.SendCommand("Debugger.resume", null, token); + var bytes = File.ReadAllBytes($"{asm_file_hot_reload}.{id}.dmeta"); + string dmeta1 = Convert.ToBase64String(bytes); + + bytes = File.ReadAllBytes($"{asm_file_hot_reload}.{id}.dil"); + string dil1 = Convert.ToBase64String(bytes); + + bytes = File.ReadAllBytes($"{asm_file_hot_reload}.{id}.dpdb"); + string dpdb1 = Convert.ToBase64String(bytes); + + var run_method = JObject.FromObject(new + { + expression = "invoke_static_method('[debugger-test] TestHotReloadUsingSDB:GetModuleGUID');" + }); + + var moduleGUID_res = await cli.SendCommand("Runtime.evaluate", run_method, token); + + Assert.True(moduleGUID_res.IsOk); + var moduleGUID = moduleGUID_res.Value["result"]["value"]; + + var applyUpdates = JObject.FromObject(new + { + moduleGUID, + dmeta = dmeta1, + dil = dil1, + dpdb = dpdb1 + }); + await cli.SendCommand("DotnetDebugger.applyUpdates", applyUpdates, token); + run_method = JObject.FromObject(new + { + expression = "window.setTimeout(function() { invoke_static_method('[debugger-test] TestHotReloadUsingSDB:RunMethod', '" + class_name + "', '" + method_name + "'); }, 1);" + }); + await cli.SendCommand("Runtime.evaluate", run_method, token); + return await insp.WaitFor(Inspector.PAUSE); + } + internal async Task LoadAssemblyAndTestHotReload(string asm_file, string pdb_file, string asm_file_hot_reload, string class_name, string method_name) { - // Simulate loading an assembly into the framework byte[] bytes = File.ReadAllBytes(asm_file); string asm_base64 = Convert.ToBase64String(bytes); bytes = File.ReadAllBytes(pdb_file); diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs index 3ecd2869fa2106..2c956cd31bbb6f 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs @@ -925,3 +925,29 @@ public static void CallToEvaluateLocal() System.Diagnostics.Debugger.Break(); } } + +public class TestHotReloadUsingSDB { + static System.Reflection.Assembly loadedAssembly; + public static string LoadLazyHotReload(string asm_base64, string pdb_base64) + { + byte[] asm_bytes = Convert.FromBase64String(asm_base64); + byte[] pdb_bytes = Convert.FromBase64String(pdb_base64); + + loadedAssembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(new System.IO.MemoryStream(asm_bytes), new System.IO.MemoryStream(pdb_bytes)); + var GUID = loadedAssembly.Modules.FirstOrDefault()?.ModuleVersionId.ToByteArray(); + return Convert.ToBase64String(GUID); + } + + public static string GetModuleGUID() + { + var GUID = loadedAssembly.Modules.FirstOrDefault()?.ModuleVersionId.ToByteArray(); + return Convert.ToBase64String(GUID); + } + + public static void RunMethod(string className, string methodName) + { + var myType = loadedAssembly.GetType($"ApplyUpdateReferencedAssembly.{className}"); + var myMethod = myType.GetMethod(methodName); + myMethod.Invoke(null, null); + } +} \ No newline at end of file