#define DEBUG_PRINTF(level, ...) do { if (G_UNLIKELY ((level) <= log_level)) { fprintf (stdout, __VA_ARGS__); } } while (0)
+enum {
+ EXCEPTION_MODE_NONE,
+ EXCEPTION_MODE_UNCAUGHT,
+ EXCEPTION_MODE_ALL
+};
+
//functions exported to be used by JS
G_BEGIN_DECLS
EMSCRIPTEN_KEEPALIVE void mono_wasm_get_var_info (int scope, int* pos, int len);
EMSCRIPTEN_KEEPALIVE void mono_wasm_clear_all_breakpoints (void);
EMSCRIPTEN_KEEPALIVE int mono_wasm_setup_single_step (int kind);
+EMSCRIPTEN_KEEPALIVE int mono_wasm_pause_on_exceptions (int state);
EMSCRIPTEN_KEEPALIVE void mono_wasm_get_object_properties (int object_id, gboolean expand_value_types);
EMSCRIPTEN_KEEPALIVE void mono_wasm_get_array_values (int object_id);
EMSCRIPTEN_KEEPALIVE void mono_wasm_get_array_value_expanded (int object_id, int idx);
//JS functions imported that we use
extern void mono_wasm_add_frame (int il_offset, int method_token, const char *assembly_name, const char *method_name);
extern void mono_wasm_fire_bp (void);
+extern void mono_wasm_fire_exception (int exception_obj_id, const char* message, const char* class_name, gboolean uncaught);
extern void mono_wasm_add_obj_var (const char*, const char*, guint64);
extern void mono_wasm_add_value_type_unexpanded_var (const char*, const char*);
extern void mono_wasm_begin_value_type_var (const char*, const char*);
G_END_DECLS
static void describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAsyncLocalThis, gboolean expandValueType);
+static void handle_exception (MonoException *exc, MonoContext *throw_ctx, MonoContext *catch_ctx, StackFrameInfo *catch_frame);
//FIXME move all of those fields to the profiler object
static gboolean debugger_enabled;
static GHashTable *objrefs;
static GHashTable *obj_to_objref;
static int objref_id = 0;
+static int pause_on_exc = EXCEPTION_MODE_NONE;
static const char*
all_getters_allowed_class_names[] = {
obj_to_objref = g_hash_table_new (NULL, NULL);
objrefs = g_hash_table_new_full (NULL, NULL, NULL, mono_debugger_free_objref);
+
+ mini_get_dbg_callbacks ()->handle_exception = handle_exception;
}
MONO_API void
}
EMSCRIPTEN_KEEPALIVE int
+mono_wasm_pause_on_exceptions (int state)
+{
+ pause_on_exc = state;
+ DEBUG_PRINTF (1, "setting pause on exception: %d\n", pause_on_exc);
+ return 1;
+}
+
+EMSCRIPTEN_KEEPALIVE int
mono_wasm_setup_single_step (int kind)
{
int nmodifiers = 1;
return isBPOnNativeCode;
}
+static int
+get_object_id(MonoObject *obj)
+{
+ ObjRef *ref;
+ if (!obj)
+ return 0;
+
+ ref = (ObjRef *)g_hash_table_lookup (obj_to_objref, GINT_TO_POINTER (~((gsize)obj)));
+ if (ref)
+ return ref->id;
+ ref = g_new0 (ObjRef, 1);
+ ref->id = mono_atomic_inc_i32 (&objref_id);
+ ref->handle = mono_gchandle_new_weakref_internal (obj, FALSE);
+ g_hash_table_insert (objrefs, GINT_TO_POINTER (ref->id), ref);
+ g_hash_table_insert (obj_to_objref, GINT_TO_POINTER (~((gsize)obj)), ref);
+ return ref->id;
+}
+
+static void
+handle_exception (MonoException *exc, MonoContext *throw_ctx, MonoContext *catch_ctx, StackFrameInfo *catch_frame)
+{
+ ERROR_DECL (error);
+ DEBUG_PRINTF (1, "handle exception - %d - %p - %p - %p\n", pause_on_exc, exc, throw_ctx, catch_ctx);
+
+ if (pause_on_exc == EXCEPTION_MODE_NONE)
+ return;
+ if (pause_on_exc == EXCEPTION_MODE_UNCAUGHT && catch_ctx != NULL)
+ return;
+
+ int obj_id = get_object_id ((MonoObject *)exc);
+ const char *error_message = mono_string_to_utf8_checked_internal (exc->message, error);
+
+ if (!is_ok (error))
+ error_message = "Failed to get exception message.";
+
+ const char *class_name = mono_class_full_name (mono_object_class (exc));
+ DEBUG_PRINTF (2, "handle exception - calling mono_wasm_fire_exc(): %d - message - %s, class_name: %s\n", obj_id, error_message, class_name);
+
+ mono_wasm_fire_exception (obj_id, error_message, class_name, !catch_ctx);
+
+ DEBUG_PRINTF (2, "handle exception - done\n");
+}
+
+
EMSCRIPTEN_KEEPALIVE void
mono_wasm_clear_all_breakpoints (void)
{
return evt->id;
}
-static int get_object_id(MonoObject *obj)
-{
- ObjRef *ref;
- if (!obj)
- return 0;
-
- ref = (ObjRef *)g_hash_table_lookup (obj_to_objref, GINT_TO_POINTER (~((gsize)obj)));
- if (ref)
- return ref->id;
- ref = g_new0 (ObjRef, 1);
- ref->id = mono_atomic_inc_i32 (&objref_id);
- ref->handle = mono_gchandle_new_weakref_internal (obj, FALSE);
- g_hash_table_insert (objrefs, GINT_TO_POINTER (ref->id), ref);
- g_hash_table_insert (obj_to_objref, GINT_TO_POINTER (~((gsize)obj)), ref);
- return ref->id;
-}
-
static gboolean
list_frames (MonoStackFrameInfo *info, MonoContext *ctx, gpointer data)
{
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
- <TargetFramework>netcoreapp3.0</TargetFramework>
+ <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
</PropertyGroup>
<ItemGroup>
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Runtime.CompilerServices;
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
public static MonoCommands GetCallStack() => new MonoCommands("MONO.mono_wasm_get_call_stack()");
+ public static MonoCommands GetExceptionObject () => new MonoCommands ("MONO.mono_wasm_get_exception_object()");
+
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 CallFunctionOn(JToken args) => new MonoCommands($"MONO.mono_wasm_call_function_on ({args.ToString ()})");
public static MonoCommands Resume() => new MonoCommands($"MONO.mono_wasm_debugger_resume ()");
+
+ public static MonoCommands SetPauseOnExceptions (string state) => new MonoCommands ($"MONO.mono_wasm_set_pause_on_exceptions(\"{state}\")");
}
internal enum MonoErrorCodes
//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<string>();
- if (top_func == "mono_wasm_fire_bp" || top_func == "_mono_wasm_fire_bp")
+ if (top_func == "mono_wasm_fire_bp" || top_func == "_mono_wasm_fire_bp" || top_func == "_mono_wasm_fire_exception")
{
- return await OnBreakpointHit(sessionId, args, token);
+ return await OnPause(sessionId, args, token);
}
break;
}
return true;
}
+ case "Debugger.setPauseOnExceptions":
+ {
+ string state = args["state"].Value<string> ();
+ await SendMonoCommand(id, MonoCommands.SetPauseOnExceptions (state), token);
+ // Pass this on to JS too
+ return false;
+ }
+
// Protocol extensions
case "DotnetDebugger.getMethodLocation":
{
}
//static int frame_id=0;
- async Task<bool> OnBreakpointHit(SessionId sessionId, JObject args, CancellationToken token)
+ async Task<bool> OnPause(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<JObject>();
var context = GetContext(sessionId);
+ JObject data = null;
+ var reason = "other";//other means breakpoint
if (res.IsErr)
{
{
var function_name = frame["functionName"]?.Value<string>();
var url = frame["url"]?.Value<string>();
- if ("mono_wasm_fire_bp" == function_name ||"_mono_wasm_fire_bp" == function_name)
+ if ("mono_wasm_fire_bp" == function_name ||"_mono_wasm_fire_bp" == function_name ||
+ "_mono_wasm_fire_exception" == function_name)
{
+ if ("_mono_wasm_fire_exception" == function_name) {
+ var exception_obj_id = await SendMonoCommand(sessionId, MonoCommands.GetExceptionObject (), token);
+ var res_val = exception_obj_id.Value? ["result"]? ["value"];
+ var exception_dotnet_obj_id = new DotnetObjectId("object", res_val?["exception_id"]?.Value<string> ());
+ data = JObject.FromObject (new {
+ type = "object",
+ subtype = "error",
+ className = res_val? ["class_name"]?.Value<string>(),
+ uncaught = res_val? ["uncaught"]?.Value<bool>(),
+ description = res_val? ["message"]?.Value<string>() + "\n",
+ objectId = exception_dotnet_obj_id.ToString()
+ });
+ reason = "exception";
+ }
+
var frames = new List<Frame>();
int frame_id = 0;
var the_mono_frames = res.Value?["result"] ? ["value"] ? ["frames"]?.Values<JObject>();
var o = JObject.FromObject(new
{
callFrames,
- reason = "other", //other means breakpoint
+ reason,
+ data,
hitBreakpoints = bp_list,
});
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+ public class ExceptionTests : DebuggerTestBase
+ {
+ [Fact]
+ public async Task ExceptionTestAll()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+ int line = 15;
+ int col = 20;
+ string entry_method_name = "[debugger-test] DebuggerTests.ExceptionTestsClass:TestExceptions";
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-exception-test.cs";
+
+ await SetPauseOnException("all");
+
+ var eval_expr = "window.setTimeout(function() { invoke_static_method (" +
+ $"'{entry_method_name}'" +
+ "); }, 1);";
+
+ var pause_location = await EvaluateAndCheck(eval_expr, null, 0, 0, null);
+ //stop in the managed caught exception
+ pause_location = await WaitForManagedException(pause_location);
+
+ AssertEqual("run", pause_location["callFrames"] ? [0] ? ["functionName"]?.Value<string>(), "pause0");
+
+ await CheckValue(pause_location["data"], JObject.FromObject(new
+ {
+ type = "object",
+ subtype = "error",
+ className = "DebuggerTests.CustomException",
+ uncaught = false
+ }), "exception0.data");
+
+ var exception_members = await GetProperties(pause_location["data"]["objectId"]?.Value<string>());
+ CheckString(exception_members, "message", "not implemented caught");
+
+ pause_location = await WaitForManagedException(null);
+ AssertEqual("run", pause_location["callFrames"] ? [0] ? ["functionName"]?.Value<string>(), "pause1");
+
+ //stop in the uncaught exception
+ CheckLocation(debugger_test_loc, 28, 16, scripts, pause_location["callFrames"][0]["location"]);
+
+ await CheckValue(pause_location["data"], JObject.FromObject(new
+ {
+ type = "object",
+ subtype = "error",
+ className = "DebuggerTests.CustomException",
+ uncaught = true
+ }), "exception1.data");
+
+ exception_members = await GetProperties(pause_location["data"]["objectId"]?.Value<string>());
+ CheckString(exception_members, "message", "not implemented uncaught");
+ });
+ }
+
+ [Fact]
+ public async Task JSExceptionTestAll()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ await SetPauseOnException("all");
+
+ var eval_expr = "window.setTimeout(function () { exceptions_test (); }, 1)";
+ var pause_location = await EvaluateAndCheck(eval_expr, null, 0, 0, "exception_caught_test", null, null);
+
+ Assert.Equal("exception", pause_location["reason"]);
+ await CheckValue(pause_location["data"], JObject.FromObject(new
+ {
+ type = "object",
+ subtype = "error",
+ className = "TypeError",
+ uncaught = false
+ }), "exception0.data");
+
+ var exception_members = await GetProperties(pause_location["data"]["objectId"]?.Value<string>());
+ CheckString(exception_members, "message", "exception caught");
+
+ pause_location = await SendCommandAndCheck(null, "Debugger.resume", null, 0, 0, "exception_uncaught_test");
+
+ Assert.Equal("exception", pause_location["reason"]);
+ await CheckValue(pause_location["data"], JObject.FromObject(new
+ {
+ type = "object",
+ subtype = "error",
+ className = "RangeError",
+ uncaught = true
+ }), "exception1.data");
+
+ exception_members = await GetProperties(pause_location["data"]["objectId"]?.Value<string>());
+ CheckString(exception_members, "message", "exception uncaught");
+ });
+ }
+
+ // FIXME? BUG? We seem to get the stack trace for Runtime.exceptionThrown at `call_method`,
+ // but JS shows the original error type, and original trace
+ [Fact]
+ public async Task ExceptionTestNone()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+ string entry_method_name = "[debugger-test] DebuggerTests.ExceptionTestsClass:TestExceptions";
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ await SetPauseOnException("none");
+
+ var eval_expr = "window.setTimeout(function() { invoke_static_method (" +
+ $"'{entry_method_name}'" +
+ "); }, 1);";
+
+ try
+ {
+ await EvaluateAndCheck(eval_expr, null, 0, 0, "", null, null);
+ }
+ catch (ArgumentException ae)
+ {
+ var eo = JObject.Parse(ae.Message);
+
+ // AssertEqual (line, eo ["exceptionDetails"]?["lineNumber"]?.Value<int> (), "lineNumber");
+ AssertEqual("Uncaught", eo["exceptionDetails"] ? ["text"]?.Value<string>(), "text");
+
+ await CheckValue(eo["exceptionDetails"] ? ["exception"], JObject.FromObject(new
+ {
+ type = "object",
+ subtype = "error",
+ className = "Error" // BUG?: "DebuggerTests.CustomException"
+ }), "exception");
+
+ return;
+ }
+
+ Assert.True(false, "Expected to get an ArgumentException from the uncaught user exception");
+ });
+ }
+
+ [Fact]
+ public async Task JSExceptionTestNone()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ await SetPauseOnException("none");
+
+ var eval_expr = "window.setTimeout(function () { exceptions_test (); }, 1)";
+
+ int line = 44;
+ try
+ {
+ await EvaluateAndCheck(eval_expr, null, 0, 0, "", null, null);
+ }
+ catch (ArgumentException ae)
+ {
+ Console.WriteLine($"{ae}");
+ var eo = JObject.Parse(ae.Message);
+
+ AssertEqual(line, eo["exceptionDetails"] ? ["lineNumber"]?.Value<int>(), "lineNumber");
+ AssertEqual("Uncaught", eo["exceptionDetails"] ? ["text"]?.Value<string>(), "text");
+
+ await CheckValue(eo["exceptionDetails"] ? ["exception"], JObject.FromObject(new
+ {
+ type = "object",
+ subtype = "error",
+ className = "RangeError"
+ }), "exception");
+
+ return;
+ }
+
+ Assert.True(false, "Expected to get an ArgumentException from the uncaught user exception");
+ });
+ }
+
+ [Theory]
+ [InlineData("function () { exceptions_test (); }", null, 0, 0, "exception_uncaught_test", "RangeError", "exception uncaught")]
+ [InlineData("function () { invoke_static_method ('[debugger-test] DebuggerTests.ExceptionTestsClass:TestExceptions'); }",
+ "dotnet://debugger-test.dll/debugger-exception-test.cs", 28, 16, "run",
+ "DebuggerTests.CustomException", "not implemented uncaught")]
+ public async Task ExceptionTestUncaught(string eval_fn, string loc, int line, int col, string fn_name,
+ string exception_type, string exception_message)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ await SetPauseOnException("uncaught");
+
+ var eval_expr = $"window.setTimeout({eval_fn}, 1);";
+ var pause_location = await EvaluateAndCheck(eval_expr, loc, line, col, fn_name);
+
+ Assert.Equal("exception", pause_location["reason"]);
+ await CheckValue(pause_location["data"], JObject.FromObject(new
+ {
+ type = "object",
+ subtype = "error",
+ className = exception_type,
+ uncaught = true
+ }), "exception.data");
+
+ var exception_members = await GetProperties(pause_location["data"]["objectId"]?.Value<string>());
+ CheckString(exception_members, "message", exception_message);
+ });
+ }
+
+ async Task<JObject> WaitForManagedException(JObject pause_location)
+ {
+ while (true)
+ {
+ if (pause_location != null)
+ {
+ AssertEqual("exception", pause_location ["reason"]?.Value<string> (), $"Expected to only pause because of an exception. {pause_location}");
+
+ // return in case of a managed exception, and ignore JS ones
+ if (pause_location["data"]?["objectId"]?.Value<string> ()?.StartsWith("dotnet:object:") == true)
+ {
+ break;
+ }
+ }
+
+ pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", null, 0, 0, null);
+ }
+
+ return pause_location;
+ }
+ }
+}
return bp1_res;
}
+ internal async Task<Result> SetPauseOnException(string state)
+ {
+ var exc_res = await ctx.cli.SendCommand("Debugger.setPauseOnExceptions", JObject.FromObject(new { state = state }), ctx.token);
+ return exc_res;
+ }
+
internal async Task<Result> SetBreakpointInMethod(string assembly, string type, string method, int lineOffset = 0, int col = 0)
{
var req = JObject.FromObject(new { assemblyName = assembly, typeName = type, methodName = method, lineOffset = lineOffset });
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Threading.Tasks;
+namespace DebuggerTests
+{
+ public class ExceptionTestsClass
+ {
+ public class TestCaughtException
+ {
+ public void run()
+ {
+ try
+ {
+ throw new CustomException ("not implemented caught");
+ }
+ catch
+ {
+ Console.WriteLine("caught exception");
+ }
+ }
+ }
+
+ public class TestUncaughtException
+ {
+ public void run()
+ {
+ throw new CustomException ("not implemented uncaught");
+ }
+ }
+
+ public static void TestExceptions()
+ {
+ TestCaughtException f = new TestCaughtException();
+ f.run();
+
+ TestUncaughtException g = new TestUncaughtException();
+ g.run();
+ }
+
+ }
+
+ public class CustomException : Exception
+ {
+ // Using this name to match with what js has.
+ // helps with the tests
+ public string message;
+ public CustomException (string message)
+ : base (message)
+ {
+ this.message = message;
+ }
+ }
+}
\ No newline at end of file
console.log (`break here`);
return ptd;
}
+
+function exception_caught_test () {
+ try {
+ throw new TypeError ('exception caught');
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+function exception_uncaught_test () {
+ console.log('uncaught test');
+ throw new RangeError ('exception uncaught');
+}
+
+function exceptions_test () {
+ exception_caught_test ();
+ exception_uncaught_test ();
+}
+
console.log ("mono_wasm_fire_bp");
debugger;
},
+
+ mono_wasm_fire_exception: function (exception_id, message, class_name, uncaught) {
+ MONO.active_exception = {
+ exception_id: exception_id,
+ message : Module.UTF8ToString (message),
+ class_name : Module.UTF8ToString (class_name),
+ uncaught : uncaught
+ };
+ debugger;
+ },
};
autoAddDeps(MonoSupportLib, '$MONO')