[wasm][debugger] Add support for raising events in the app, intended to (#42171)
authorAnkit Jain <radical@gmail.com>
Tue, 15 Sep 2020 18:04:33 +0000 (14:04 -0400)
committerGitHub <noreply@github.com>
Tue, 15 Sep 2020 18:04:33 +0000 (14:04 -0400)
* [wasm][debugger] Add support for raising events in the app, intended to

.. be received by the debug proxy.

* [wasm][debugger] `mono_wasm_raise_event` -> `mono_wasm_raise_debug_event`

Based on @lewing's suggestion

src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs
src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs
src/mono/wasm/runtime/library_mono.js

index 9a5761f..2bfabd9 100644 (file)
@@ -220,6 +220,7 @@ namespace Microsoft.WebAssembly.Diagnostics
     internal class MonoConstants
     {
         public const string RUNTIME_IS_READY = "mono_wasm_runtime_ready";
+        public const string EVENT_RAISED = "mono_wasm_debug_event_raised:aef14bca-5519-4dfe-b35a-f867abc123ae";
     }
 
     class Frame
index 0e48068..7de7215 100644 (file)
@@ -10,6 +10,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.CodeAnalysis;
 using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 using Mono.Cecil.Cil;
 using Mono.Cecil.Pdb;
@@ -82,7 +83,29 @@ namespace Microsoft.WebAssembly.Diagnostics
                                 }
                                 await RuntimeReady(sessionId, token);
                             }
+                            else if (a?[0]?["value"]?.ToString() == MonoConstants.EVENT_RAISED)
+                            {
+                                if (a.Type != JTokenType.Array)
+                                {
+                                    logger.LogDebug("Invalid event raised args, expected an array: {a}");
+                                }
+                                else
+                                {
+                                    if (JObjectTryParse(a?[2]?["value"]?.Value<string>(), out JObject raiseArgs) &&
+                                        JObjectTryParse(a?[1]?["value"]?.Value<string>(), out JObject eventArgs))
+                                    {
+                                        await OnJSEventRaised(sessionId, eventArgs, token);
 
+                                        if (raiseArgs?["trace"]?.Value<bool>() == true) {
+                                            // Let the message show up on the console
+                                            return false;
+                                        }
+                                    }
+                                }
+
+                                // Don't log this message in the console
+                                return true;
+                            }
                         }
                         break;
                     }
@@ -777,6 +800,27 @@ namespace Microsoft.WebAssembly.Diagnostics
             return true;
         }
 
+        async Task<bool> OnJSEventRaised(SessionId sessionId, JObject eventArgs, CancellationToken token)
+        {
+            string eventName = eventArgs?["eventName"]?.Value<string>();
+            if (string.IsNullOrEmpty(eventName))
+            {
+                logger.LogDebug($"Missing name for raised js event: {eventArgs}");
+                return false;
+            }
+
+            logger.LogDebug($"OnJsEventRaised: args: {eventArgs}");
+
+            switch (eventName)
+            {
+                default:
+                {
+                    logger.LogDebug($"Unknown js event name: {eventName} with args {eventArgs}");
+                    return await Task.FromResult(false);
+                }
+            }
+        }
+
         async Task<bool> OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string expression, CancellationToken token)
         {
             try
@@ -1075,5 +1119,24 @@ namespace Microsoft.WebAssembly.Diagnostics
                     sessions.Remove(sessionId);
             }
         }
+
+        bool JObjectTryParse(string str, out JObject obj, bool log_exception = true)
+        {
+            obj = null;
+            if (string.IsNullOrEmpty(str))
+                return false;
+
+            try
+            {
+                obj = JObject.Parse(str);
+                return true;
+            }
+            catch (JsonReaderException jre)
+            {
+                if (log_exception)
+                    logger.LogDebug($"Could not parse {str}. Failed with {jre}");
+                return false;
+            }
+        }
     }
 }
index ca53028..1cec819 100644 (file)
@@ -138,5 +138,79 @@ namespace DebuggerTests
                 Assert.False(res.IsOk);
             });
         }
+
+        [Fact]
+        public async Task BadRaiseDebugEventsTest()
+        {
+            var insp = new Inspector();
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async (cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                var bad_expressions = new[]
+                {
+                    "MONO.mono_wasm_raise_debug_event('')",
+                    "MONO.mono_wasm_raise_debug_event(undefined)",
+                    "MONO.mono_wasm_raise_debug_event({})",
+
+                    "MONO.mono_wasm_raise_debug_event({eventName:'foo'}, '')",
+                    "MONO.mono_wasm_raise_debug_event({eventName:'foo'}, 12)"
+                };
+
+                foreach (var expression in bad_expressions)
+                {
+                    var res = await ctx.cli.SendCommand($"Runtime.evaluate",
+                                JObject.FromObject(new
+                                {
+                                    expression,
+                                    returnByValue = true
+                                }), ctx.token);
+                    Assert.False(res.IsOk, $"Expected to fail for {expression}");
+                }
+            });
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        [InlineData(null)]
+        public async Task RaiseDebugEventTraceTest(bool? trace)
+        {
+            var insp = new Inspector();
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async (cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                var tcs = new TaskCompletionSource<bool>();
+                insp.On("Runtime.consoleAPICalled", async (args, token) => {
+                    if (args?["type"]?.Value<string>() == "debug" &&
+                       args?["args"]?.Type == JTokenType.Array &&
+                       args?["args"]?[0]?["value"]?.Value<string>()?.StartsWith("mono_wasm_debug_event_raised:") == true)
+                    {
+                        tcs.SetResult(true);
+                    }
+
+                    await Task.CompletedTask;
+                });
+
+                var trace_str = trace.HasValue ? $"trace: {trace.ToString().ToLower()}" : String.Empty;
+                var expression = $"MONO.mono_wasm_raise_debug_event({{ eventName:'qwe' }}, {{ {trace_str} }})";
+                var res = await ctx.cli.SendCommand($"Runtime.evaluate", JObject.FromObject(new { expression }), ctx.token);
+                Assert.True(res.IsOk, $"Expected to pass for {expression}");
+
+                var t = await Task.WhenAny(tcs.Task, Task.Delay(2000));
+
+                if (trace == true)
+                    Assert.True(tcs.Task == t, "Timed out waiting for the event to be logged");
+                else
+                    Assert.False(tcs.Task == t, "Event should not have been logged");
+            });
+        }
     }
 }
index 4ed2ea8..b908d6b 100644 (file)
  * @type {number} - address in wasm memory
  */
 
+/**
+ * @typedef Event
+ * @type {object}
+ * @property {string} eventName - name of the event being raised
+ * @property {object} eventArgs - arguments for the event itself
+ */
+
 var MonoSupportLib = {
        $MONO__postset: 'MONO.export_functions (Module);',
        $MONO: {
@@ -2050,7 +2057,26 @@ var MonoSupportLib = {
                                data = data.slice(length);
                        }
                        return true;
-               }
+               },
+
+               /**
+                * Raises an event for the debug proxy
+                *
+                * @param {Event} event - event to be raised
+                * @param {object} args - arguments for raising this event, eg. `{trace: true}`
+                */
+               mono_wasm_raise_debug_event: function(event, args={}) {
+                       if (typeof event !== 'object')
+                               throw new Error(`event must be an object, but got ${JSON.stringify(event)}`);
+
+                       if (event.eventName === undefined)
+                               throw new Error(`event.eventName is a required parameter, in event: ${JSON.stringify(event)}`);
+
+                       if (typeof args !== 'object')
+                               throw new Error(`args must be an object, but got ${JSON.stringify(args)}`);
+
+                       console.debug('mono_wasm_debug_event_raised:aef14bca-5519-4dfe-b35a-f867abc123ae', JSON.stringify(event), JSON.stringify(args));
+               },
        },
 
        mono_wasm_add_typed_value: function (type, str_value, value) {