From e13871cb275b9f53fa82285b2a81ada28a859b50 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Sun, 9 Aug 2020 12:36:16 -0400 Subject: [PATCH] [wasm][debugger] Add support for invoking getters on ValueTypes (#40548) * [wasm][debugger][tests] Fix negative pointer tests * [wasm][debugger][tests] Fix test to correctly check the valuetype local test: `CheckUpdatedValueTypeFieldsOnResume` * [wasm][debugger][tests] Make value checks consistent - In some places we weren't checking for the `description` property - and this hid a bug where sometimes that property wasn't added (eg. for numbers) - Instead, we were working around that by "fixing it up" later - Now, we use the same checks for `Check{Number,String,*}` API, and the `CheckValue/CheckProps` API used with `TNumber` etc. - So, this commit: - fixes the checks, and the tests - and fixes the bug * [wasm][debugger] Add new `id` types, which have associated properties - these are of the form `dotnet:${scheme}:{id-args-object}` - Examples: - `dotnet:valuetype:{ containerId: 4 }` - `dotnet:valuetype:{ num: 2 }` - the `num` field is autogenerated if no id-args are provided. This gets used when valuetypes are expanded. - `this._id_table [id-string]` has associated property objects for every `id` - This might contain, for example, `klass` pointer, and base64 representation of a valuetype * [wasm][debugger] Update valuetype code to use the new `id`s * [wasm][debugger] Simplify array API in `mini-wasm-debugger.c` .. to use a single function to get details of the full array, and individual elements. * [wasm][debugger] library_mono.js: improvements to valuetype code - Allow `_new_id` to update properties for existing objectIds - Extract valuetype id assigment code to a separate function * [wasm][debugger] mini-wasm-debugger.c- extract object id lookup into a function * [wasm][debugger][tests] Rename method param to be self descriptive * [wasm][debugger][tests] Rework cfo test for getters - add some new getters to the test classes - this will become useful in subsequent commits that add support for invoking getters on valuetypes * [wasm][debugger][tests] Improve valuetype locals/method args tests - this also becomes useful in subsequent commits which enable invoking getters on valuetypes * [wasm][debugger] Add support for invoking getters on valuetypes - keep a copy of the value bytes, and the klass pointer - this allows being able to invoke getters on such a valuetype, at a later point - This allows getters like `DateTime.Date`, which has the type `DateTime` * [wasm][debugger] mono.js: fix warnings .. and replace `var` with `let`, or `const`, where appropriate. * [wasm][debugger] mono.js: _split_object_id -> _parse_object_id * [wasm][debugger] Streamline accessing exported debugger.c functions .. especially the ones that return data in `MONO.var_info`. To use: 1. `this._register_c_var_fn ('mono_wasm_get_object_properties', 'bool', [ 'number', 'bool' ]);` 2. Now, this function can be called as `this.mono_wasm_get_object_properties_info (.. )` - returns `res` which has the contents of `MONO.var_info`, after running `_fixup_name_value_objects` on it. * [wasm][debugger] Return errors from debugger.c's details functions - functions like those for getting object/vt properties, can fail, for example, if the objectId is invalid. - We now return that bool result, and that gets surfaced to the caller - This will also help to differentiate the case where the result of such a function was a failure vs just an empty result * [wasm][debugger] Small checks on inputs, and some negative tests - These tests don't actually depend on the error message, and we don't have another to way to differentiate why a command might have failed with an exception. So, right now, they will pass as long as the commands fail as expected. - Future TODO: return `error`, instead of exception details for issues in `mono.js`, like incorrect input, invalid id etc, and update these tests accordingly. * Update src/mono/mono/mini/mini-wasm-debugger.c Co-authored-by: Larry Ewing * Remove description checking from TString Co-authored-by: Larry Ewing --- src/mono/mono/mini/mini-wasm-debugger.c | 217 +++--- .../wasm/debugger/DebuggerTestSuite/ArrayTests.cs | 118 +++- .../DebuggerTestSuite/CallFunctionOnTests.cs | 274 ++++++-- .../debugger/DebuggerTestSuite/PointerTests.cs | 6 +- .../wasm/debugger/DebuggerTestSuite/Support.cs | 193 +++-- src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs | 222 +++--- src/mono/wasm/debugger/tests/debugger-cfo-test.cs | 27 +- .../debugger/tests/debugger-valuetypes-test.cs | 1 + src/mono/wasm/debugger/tests/other.js | 13 + src/mono/wasm/runtime/library_mono.js | 776 +++++++++++++-------- 10 files changed, 1179 insertions(+), 668 deletions(-) diff --git a/src/mono/mono/mini/mini-wasm-debugger.c b/src/mono/mono/mini/mini-wasm-debugger.c index 90855b5..66608a7 100644 --- a/src/mono/mono/mini/mini-wasm-debugger.c +++ b/src/mono/mono/mini/mini-wasm-debugger.c @@ -37,24 +37,21 @@ EMSCRIPTEN_KEEPALIVE int mono_wasm_set_breakpoint (const char *assembly_name, in EMSCRIPTEN_KEEPALIVE int mono_wasm_remove_breakpoint (int bp_id); EMSCRIPTEN_KEEPALIVE int mono_wasm_current_bp_id (void); EMSCRIPTEN_KEEPALIVE void mono_wasm_enum_frames (void); -EMSCRIPTEN_KEEPALIVE void mono_wasm_get_var_info (int scope, int* pos, int len); +EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_local_vars (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); -EMSCRIPTEN_KEEPALIVE void mono_wasm_invoke_getter_on_object (int object_id, const char* name); -EMSCRIPTEN_KEEPALIVE void mono_wasm_get_deref_ptr_value (void *value_addr, MonoClass *klass); +EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_object_properties (int object_id, gboolean expand_value_types); +EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_array_values (int object_id, int start_idx, int count, gboolean expand_value_types); +EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_invoke_getter_on_object (int object_id, const char* name); +EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_invoke_getter_on_value (void *value, MonoClass *klass, const char *name); +EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_deref_ptr_value (void *value_addr, MonoClass *klass); //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*); -extern void mono_wasm_end_value_type_var (void); extern void mono_wasm_add_enum_var (const char*, const char*, guint64); extern void mono_wasm_add_func_var (const char*, const char*, guint64); extern void mono_wasm_add_properties_var (const char*, gint32); @@ -631,6 +628,22 @@ mono_wasm_current_bp_id (void) return evt->id; } +static MonoObject* +get_object_from_id (int objectId) +{ + ObjRef *ref = (ObjRef *)g_hash_table_lookup (objrefs, GINT_TO_POINTER (objectId)); + if (!ref) { + DEBUG_PRINTF (2, "get_object_from_id !ref: %d\n", objectId); + return NULL; + } + + MonoObject *obj = mono_gchandle_get_target_internal (ref->handle); + if (!obj) + DEBUG_PRINTF (2, "get_object_from_id !obj: %d\n", objectId); + + return obj; +} + static gboolean list_frames (MonoStackFrameInfo *info, MonoContext *ctx, gpointer data) { @@ -968,17 +981,28 @@ static gboolean describe_value(MonoType * type, gpointer addr, gboolean expandVa mono_wasm_add_enum_var (class_name, enum_members->str, value__); g_string_free (enum_members, TRUE); - } else if (expandValueType) { - char *to_string_val = get_to_string_description (class_name, klass, addr); - mono_wasm_begin_value_type_var (class_name, to_string_val); - g_free (to_string_val); - - // FIXME: isAsyncLocalThis - describe_object_properties_for_klass ((MonoObject*)addr, klass, FALSE, expandValueType); - mono_wasm_end_value_type_var (); } else { char *to_string_val = get_to_string_description (class_name, klass, addr); - mono_wasm_add_value_type_unexpanded_var (class_name, to_string_val); + + if (expandValueType) { + int32_t size = mono_class_value_size (klass, NULL); + void *value_buf = g_malloc0 (size); + mono_value_copy_internal (value_buf, addr, klass); + + EM_ASM ({ + MONO.mono_wasm_add_typed_value ($0, $1, { toString: $2, value_addr: $3, value_size: $4, klass: $5 }); + }, "begin_vt", class_name, to_string_val, value_buf, size, klass); + + g_free (value_buf); + + // FIXME: isAsyncLocalThis + describe_object_properties_for_klass (addr, klass, FALSE, expandValueType); + mono_wasm_add_typed_value ("end_vt", NULL, 0); + } else { + EM_ASM ({ + MONO.mono_wasm_add_typed_value ($0, $1, { toString: $2 }); + }, "unexpanded_vt", class_name, to_string_val); + } g_free (to_string_val); } g_free (class_name); @@ -1036,7 +1060,7 @@ describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAs gboolean is_valuetype; int pnum; char *klass_name; - gboolean getters_allowed; + gboolean auto_invoke_getters; g_assert (klass); is_valuetype = m_class_is_valuetype(klass); @@ -1069,7 +1093,7 @@ describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAs } klass_name = mono_class_full_name (klass); - getters_allowed = are_getters_allowed (klass_name); + auto_invoke_getters = are_getters_allowed (klass_name); iter = NULL; pnum = 0; @@ -1081,29 +1105,24 @@ describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAs mono_wasm_add_properties_var (p->name, pnum); sig = mono_method_signature_internal (p->get); - // automatic properties will get skipped - if (!getters_allowed) { + gboolean vt_self_type_getter = is_valuetype && mono_class_from_mono_type_internal (sig->ret) == klass; + if (auto_invoke_getters && !vt_self_type_getter) { + invoke_and_describe_getter_value (obj, p); + } else { // not allowed to call the getter here char *ret_class_name = mono_class_full_name (mono_class_from_mono_type_internal (sig->ret)); - // getters not supported for valuetypes, yet - gboolean invokable = !is_valuetype && sig->param_count == 0; + gboolean invokable = sig->param_count == 0; mono_wasm_add_typed_value ("getter", ret_class_name, invokable); g_free (ret_class_name); continue; } - - if (is_valuetype && mono_class_from_mono_type_internal (sig->ret) == klass) { - // Property of the same valuetype, avoid endlessly recursion! - mono_wasm_add_typed_value ("getter", klass_name, 0); - continue; - } - - invoke_and_describe_getter_value (obj, p); } pnum ++; } + + g_free (klass_name); } /* @@ -1132,17 +1151,10 @@ static gboolean describe_object_properties (guint64 objectId, gboolean isAsyncLocalThis, gboolean expandValueType) { DEBUG_PRINTF (2, "describe_object_properties %llu\n", objectId); - ObjRef *ref = (ObjRef *)g_hash_table_lookup (objrefs, GINT_TO_POINTER (objectId)); - if (!ref) { - DEBUG_PRINTF (2, "describe_object_properties !ref\n"); - return FALSE; - } - MonoObject *obj = mono_gchandle_get_target_internal (ref->handle); - if (!obj) { - DEBUG_PRINTF (2, "describe_object_properties !obj\n"); + MonoObject *obj = get_object_from_id (objectId); + if (!obj) return FALSE; - } if (m_class_is_delegate (mono_object_class (obj))) { // delegates get the same id format as regular objects @@ -1155,21 +1167,13 @@ describe_object_properties (guint64 objectId, gboolean isAsyncLocalThis, gboolea } static gboolean -invoke_getter_on_object (guint64 objectId, const char *name) +invoke_getter (void *obj_or_value, MonoClass *klass, const char *name) { - ObjRef *ref = (ObjRef *)g_hash_table_lookup (objrefs, GINT_TO_POINTER (objectId)); - if (!ref) { - DEBUG_PRINTF (1, "invoke_getter_on_object no objRef found for id %llu\n", objectId); - return FALSE; - } - - MonoObject *obj = mono_gchandle_get_target_internal (ref->handle); - if (!obj) { - DEBUG_PRINTF (1, "invoke_getter_on_object !obj\n"); + if (!obj_or_value || !klass || !name) { + DEBUG_PRINTF (2, "invoke_getter: none of the arguments can be null"); return FALSE; } - MonoClass *klass = mono_object_class (obj); gpointer iter = NULL; MonoProperty *p; while ((p = mono_class_get_properties (klass, &iter))) { @@ -1177,7 +1181,7 @@ invoke_getter_on_object (guint64 objectId, const char *name) if (!p->get->name || strcasecmp (p->name, name) != 0) continue; - invoke_and_describe_getter_value (obj, p); + invoke_and_describe_getter_value (obj_or_value, p); return TRUE; } @@ -1185,50 +1189,48 @@ invoke_getter_on_object (guint64 objectId, const char *name) } static gboolean -describe_array_values (guint64 objectId) +describe_array_values (guint64 objectId, int startIdx, int count, gboolean expandValueType) { + if (count == 0) + return TRUE; + int esize; gpointer elem; - ObjRef *ref = (ObjRef *)g_hash_table_lookup (objrefs, GINT_TO_POINTER (objectId)); - if (!ref) { + MonoArray *arr = (MonoArray*) get_object_from_id (objectId); + if (!arr) return FALSE; - } - MonoArray *arr = (MonoArray *)mono_gchandle_get_target_internal (ref->handle); - MonoObject *obj = &arr->obj; - if (!obj) { + + MonoClass *klass = mono_object_class (arr); + MonoTypeEnum type = m_class_get_byval_arg (klass)->type; + if (type != MONO_TYPE_SZARRAY && type != MONO_TYPE_ARRAY) { + DEBUG_PRINTF (1, "describe_array_values: object is not an array. type: 0x%x\n", type); return FALSE; } - esize = mono_array_element_size (obj->vtable->klass); - for (int i = 0; i < arr->max_length; i++) { - mono_wasm_add_array_item(i); - elem = (gpointer*)((char*)arr->vector + (i * esize)); - describe_value (m_class_get_byval_arg (m_class_get_element_class (arr->obj.vtable->klass)), elem, FALSE); + + int len = arr->max_length; + if (len == 0 && startIdx == 0 && count <= 0) { + // Nothing to do + return TRUE; } - return TRUE; -} -/* Expands valuetypes */ -static gboolean -describe_array_value_expanded (guint64 objectId, guint64 idx) -{ - int esize; - gpointer elem; - ObjRef *ref = (ObjRef *)g_hash_table_lookup (objrefs, GINT_TO_POINTER (objectId)); - if (!ref) { + if (startIdx < 0 || (len > 0 && startIdx >= len)) { + DEBUG_PRINTF (1, "describe_array_values: invalid startIdx (%d) for array of length %d\n", startIdx, len); return FALSE; } - MonoArray *arr = (MonoArray *)mono_gchandle_get_target_internal (ref->handle); - MonoObject *obj = &arr->obj; - if (!obj) { + + if (count > 0 && (startIdx + count) > len) { + DEBUG_PRINTF (1, "describe_array_values: invalid count (%d) for startIdx: %d, and array of length %d\n", count, startIdx, len); return FALSE; } - if (idx >= arr->max_length) - return FALSE; - esize = mono_array_element_size (obj->vtable->klass); - elem = (gpointer*)((char*)arr->vector + (idx * esize)); - describe_value (m_class_get_byval_arg (m_class_get_element_class (arr->obj.vtable->klass)), elem, TRUE); + esize = mono_array_element_size (klass); + int endIdx = count < 0 ? len : startIdx + count; + for (int i = startIdx; i < endIdx; i ++) { + mono_wasm_add_array_item(i); + elem = (gpointer*)((char*)arr->vector + (i * esize)); + describe_value (m_class_get_byval_arg (m_class_get_element_class (klass)), elem, expandValueType); + } return TRUE; } @@ -1331,22 +1333,23 @@ describe_variables_on_frame (MonoStackFrameInfo *info, MonoContext *ctx, gpointe return TRUE; } -EMSCRIPTEN_KEEPALIVE void +EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_deref_ptr_value (void *value_addr, MonoClass *klass) { MonoType *type = m_class_get_byval_arg (klass); if (type->type != MONO_TYPE_PTR && type->type != MONO_TYPE_FNPTR) { DEBUG_PRINTF (2, "BUG: mono_wasm_get_deref_ptr_value: Expected to get a ptr type, but got 0x%x\n", type->type); - return; + return FALSE; } mono_wasm_add_properties_var ("deref", -1); describe_value (type->data.type, value_addr, TRUE); + return TRUE; } //FIXME this doesn't support getting the return value pseudo-var -EMSCRIPTEN_KEEPALIVE void -mono_wasm_get_var_info (int scope, int* pos, int len) +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_get_local_vars (int scope, int* pos, int len) { FrameDescData data; data.target_frame = scope; @@ -1355,37 +1358,51 @@ mono_wasm_get_var_info (int scope, int* pos, int len) data.pos = pos; mono_walk_stack_with_ctx (describe_variables_on_frame, NULL, MONO_UNWIND_NONE, &data); + + return TRUE; } -EMSCRIPTEN_KEEPALIVE void +EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_object_properties (int object_id, gboolean expand_value_types) { DEBUG_PRINTF (2, "getting properties of object %d\n", object_id); - describe_object_properties (object_id, FALSE, expand_value_types); + return describe_object_properties (object_id, FALSE, expand_value_types); } -EMSCRIPTEN_KEEPALIVE void -mono_wasm_get_array_values (int object_id) +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_get_array_values (int object_id, int start_idx, int count, gboolean expand_value_types) { - DEBUG_PRINTF (2, "getting array values %d\n", object_id); + DEBUG_PRINTF (2, "getting array values %d, startIdx: %d, count: %d, expandValueType: %d\n", object_id, start_idx, count, expand_value_types); - describe_array_values(object_id); + return describe_array_values (object_id, start_idx, count, expand_value_types); } -EMSCRIPTEN_KEEPALIVE void -mono_wasm_get_array_value_expanded (int object_id, int idx) +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_invoke_getter_on_object (int object_id, const char* name) { - DEBUG_PRINTF (2, "getting array value %d for idx %d\n", object_id, idx); + MonoObject *obj = get_object_from_id (object_id); + if (!obj) + return FALSE; - describe_array_value_expanded (object_id, idx); + return invoke_getter (obj, mono_object_class (obj), name); } -EMSCRIPTEN_KEEPALIVE void -mono_wasm_invoke_getter_on_object (int object_id, const char* name) +EMSCRIPTEN_KEEPALIVE gboolean +mono_wasm_invoke_getter_on_value (void *value, MonoClass *klass, const char *name) { - invoke_getter_on_object (object_id, name); + DEBUG_PRINTF (2, "mono_wasm_invoke_getter_on_value: v: %p klass: %p, name: %s\n", value, klass, name); + if (!klass || !value) + return FALSE; + + if (!m_class_is_valuetype (klass)) { + DEBUG_PRINTF (2, "mono_wasm_invoke_getter_on_value: klass is not a valuetype. name: %s\n", mono_class_full_name (klass)); + return FALSE; + } + + return invoke_getter (value, klass, name); } + // Functions required by debugger-state-machine. gsize mono_debugger_tls_thread_id (DebuggerTlsData *debuggerTlsData) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs index 022e41c..f9319a7 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs @@ -26,7 +26,7 @@ namespace DebuggerTests etype_name: "int", local_var_name_prefix: "int", array : new [] { TNumber(4), TNumber(70), TNumber(1) }, - array_elements : null, + array_elem_props: null, test_prev_frame : test_prev_frame, frame_idx : frame_idx, use_cfo : use_cfo); @@ -47,7 +47,7 @@ namespace DebuggerTests TValueType("DebuggerTests.Point"), TValueType("DebuggerTests.Point"), }, - array_elements : new [] + array_elem_props: new [] { TPoint(5, -2, "point_arr#Id#0", "Green"), TPoint(123, 0, "point_arr#Id#1", "Blue") @@ -73,7 +73,7 @@ namespace DebuggerTests TObject("DebuggerTests.SimpleClass", is_null : true), TObject("DebuggerTests.SimpleClass") }, - array_elements : new [] + array_elem_props: new [] { TSimpleClass(5, -2, "class_arr#Id#0", "Green"), null, // Element is null @@ -100,7 +100,7 @@ namespace DebuggerTests TObject("DebuggerTests.GenericClass"), TObject("DebuggerTests.GenericClass") }, - array_elements : new [] + array_elem_props : new [] { null, // Element is null new @@ -136,7 +136,7 @@ namespace DebuggerTests TValueType("DebuggerTests.SimpleGenericStruct"), TValueType("DebuggerTests.SimpleGenericStruct") }, - array_elements : new [] + array_elem_props : new [] { new { @@ -171,7 +171,7 @@ namespace DebuggerTests TValueType("DebuggerTests.SimpleGenericStruct"), TValueType("DebuggerTests.SimpleGenericStruct") }, - array_elements : new [] + array_elem_props : new [] { new { @@ -199,7 +199,7 @@ namespace DebuggerTests use_cfo : use_cfo); async Task TestSimpleArrayLocals(int line, int col, string entry_method_name, string method_name, string etype_name, - string local_var_name_prefix, object[] array, object[] array_elements, + string local_var_name_prefix, object[] array, object[] array_elem_props, bool test_prev_frame = false, int frame_idx = 0, bool use_cfo = false) { var insp = new Inspector(); @@ -223,8 +223,8 @@ namespace DebuggerTests var locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value()); Assert.Equal(4, locals.Count()); - CheckArray(locals, $"{local_var_name_prefix}_arr", $"{etype_name}[]"); - CheckArray(locals, $"{local_var_name_prefix}_arr_empty", $"{etype_name}[]"); + CheckArray(locals, $"{local_var_name_prefix}_arr", $"{etype_name}[]", array?.Length ?? 0); + CheckArray(locals, $"{local_var_name_prefix}_arr_empty", $"{etype_name}[]", 0); CheckObject(locals, $"{local_var_name_prefix}_arr_null", $"{etype_name}[]", is_null : true); CheckBool(locals, "call_other", test_prev_frame); @@ -250,13 +250,13 @@ namespace DebuggerTests await CheckProps(prefix_arr, array, local_arr_name); - if (array_elements?.Length > 0) + if (array_elem_props?.Length > 0) { - for (int i = 0; i < array_elements.Length; i++) + for (int i = 0; i < array_elem_props.Length; i++) { var i_str = i.ToString(); var label = $"{local_var_name_prefix}_arr[{i}]"; - if (array_elements[i] == null) + if (array_elem_props[i] == null) { var act_i = prefix_arr.FirstOrDefault(jt => jt["name"]?.Value() == i_str); Assert.True(act_i != null, $"[{label}] Couldn't find array element [{i_str}]"); @@ -265,7 +265,7 @@ namespace DebuggerTests } else { - await CompareObjectPropertiesFor(prefix_arr, i_str, array_elements[i], label : label); + await CompareObjectPropertiesFor(prefix_arr, i_str, array_elem_props[i], label : label); } } } @@ -601,5 +601,95 @@ namespace DebuggerTests }); } + [Fact] + public async Task InvalidArrayId() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.Container", "PlaceholderMethod", 1, "PlaceholderMethod", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ArrayTestsClass:ObjectArrayMembers'); }, 1);", + wait_for_event_fn : async(pause_location) => + { + + int frame_idx = 1; + var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value()); + var c_obj = GetAndAssertObjectWithName(frame_locals, "c"); + var c_obj_id = c_obj["value"] ? ["objectId"]?.Value(); + Assert.NotNull(c_obj_id); + + // Invalid format + await GetProperties("dotnet:array:4123", expect_ok : false); + + // Invalid object id + await GetProperties("dotnet:array:{ \"arrayId\": 234980 }", expect_ok : false); + + // Trying to access object as an array + if (!DotnetObjectId.TryParse (c_obj_id, out var id) || id.Scheme != "object") + Assert.True(false, "Unexpected object id format. Maybe this test is out of sync with the object id format in library_mono.js?"); + + if (!int.TryParse(id.Value, out var idNum)) + Assert.True(false, "Expected a numeric value part of the object id: {c_obj_id}"); + await GetProperties($"dotnet:array:{{\"arrayId\":{idNum}}}", expect_ok : false); + }); + + [Fact] + public async Task InvalidValueTypeArrayIndex() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.Container", "PlaceholderMethod", 1, "PlaceholderMethod", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ArrayTestsClass:ObjectArrayMembers'); }, 1);", + locals_fn : async(locals) => + { + var this_obj = GetAndAssertObjectWithName(locals, "this"); + var c_obj = GetAndAssertObjectWithName(await GetProperties(this_obj["value"]["objectId"].Value()), "c"); + var c_obj_id = c_obj["value"] ? ["objectId"]?.Value(); + Assert.NotNull(c_obj_id); + + var c_props = await GetProperties(c_obj_id); + + var pf_arr = GetAndAssertObjectWithName(c_props, "PointsField"); + var pf_arr_elems = await GetProperties(pf_arr["value"]["objectId"].Value()); + + if (!DotnetObjectId.TryParse(pf_arr_elems[0]["value"] ? ["objectId"]?.Value(), out var id)) + Assert.True(false, "Couldn't parse objectId for PointsFields' elements"); + + AssertEqual("valuetype", id.Scheme, "Expected a valuetype id"); + var id_args = id.ValueAsJson; + Assert.True(id_args["arrayId"] != null, "ObjectId format for array seems to have changed. Expected to find 'arrayId' in the value. Update this test"); + Assert.True(id_args != null, "Expected to get a json as the value part of {id}"); + + // Try one valid query, to confirm that the id format hasn't changed! + id_args["arrayIdx"] = 0; + await GetProperties($"dotnet:valuetype:{id_args.ToString (Newtonsoft.Json.Formatting.None)}", expect_ok : true); + + id_args["arrayIdx"] = 12399; + await GetProperties($"dotnet:valuetype:{id_args.ToString (Newtonsoft.Json.Formatting.None)}", expect_ok : false); + + id_args["arrayIdx"] = -1; + await GetProperties($"dotnet:valuetype:{id_args.ToString (Newtonsoft.Json.Formatting.None)}", expect_ok : false); + + id_args["arrayIdx"] = "qwe"; + await GetProperties($"dotnet:valuetype:{id_args.ToString (Newtonsoft.Json.Formatting.None)}", expect_ok : false); + }); + + [Fact] + public async Task InvalidAccessors() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.Container", "PlaceholderMethod", 1, "PlaceholderMethod", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ArrayTestsClass:ObjectArrayMembers'); }, 1);", + locals_fn : async(locals) => + { + var this_obj = GetAndAssertObjectWithName(locals, "this"); + var c_obj = GetAndAssertObjectWithName(await GetProperties(this_obj["value"]["objectId"].Value()), "c"); + var c_obj_id = c_obj["value"] ? ["objectId"]?.Value(); + Assert.NotNull(c_obj_id); + + var c_props = await GetProperties(c_obj_id); + + var pf_arr = GetAndAssertObjectWithName(c_props, "PointsField"); + + var invalid_accessors = new object[] { "NonExistant", "10000", "-2", 10000, -2, null, String.Empty }; + foreach (var invalid_accessor in invalid_accessors) + { + // var res = await InvokeGetter (JObject.FromObject (new { value = new { objectId = obj_id } }), invalid_accessor, expect_ok: true); + var res = await InvokeGetter(pf_arr, invalid_accessor, expect_ok : true); + AssertEqual("undefined", res.Value["result"] ? ["type"]?.ToString(), "Expected to get undefined result for non-existant accessor"); + } + }); + } -} \ No newline at end of file +} diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs index 5c66bb2..3014c6c 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs @@ -481,6 +481,36 @@ namespace DebuggerTests // callFunctionOn result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token); await CheckValue(result.Value["result"], TNumber(5), "cfo-res"); + + cfo_args = JObject.FromObject(new + { + functionDeclaration = "function () { return 'test value'; }", + objectId = obj_id + }); + + // value of @returnByValue doesn't matter, as the returned value + // is a primitive + if (return_by_val) + cfo_args["returnByValue"] = return_by_val; + + // callFunctionOn + result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token); + await CheckValue(result.Value["result"], JObject.FromObject(new { type = "string", value = "test value" }), "cfo-res"); + + cfo_args = JObject.FromObject(new + { + functionDeclaration = "function () { return null; }", + objectId = obj_id + }); + + // value of @returnByValue doesn't matter, as the returned value + // is a primitive + if (return_by_val) + cfo_args["returnByValue"] = return_by_val; + + // callFunctionOn + result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token); + await CheckValue(result.Value["result"], JObject.Parse("{ type: 'object', subtype: 'null', value: null }"), "cfo-res"); }); } @@ -536,7 +566,7 @@ namespace DebuggerTests }); } - public static TheoryData, bool> GettersTestData(bool use_cfo) => new TheoryData, bool> + public static TheoryData, string, bool> GettersTestData(string local_name, bool use_cfo) => new TheoryData, string, bool> { // Chrome sends this one { @@ -546,6 +576,7 @@ namespace DebuggerTests 12, "function invokeGetter(arrayStr){ let result=this; const properties=JSON.parse(arrayStr); for(let i=0,n=properties.length;i JArray.FromObject(arg_strs).ToString(), + local_name, use_cfo }, { @@ -555,6 +586,7 @@ namespace DebuggerTests 12, "function invokeGetter(arrayStr){ let result=this; const properties=JSON.parse(arrayStr); for(let i=0,n=properties.length;i JArray.FromObject(arg_strs).ToString(), + local_name, use_cfo }, @@ -566,6 +598,7 @@ namespace DebuggerTests 12, "function(e){return this[e]}", (args_str) => args_str?.Length > 0 ? args_str[0] : String.Empty, + local_name, use_cfo }, { @@ -575,14 +608,17 @@ namespace DebuggerTests 12, "function(e){return this[e]}", (args_str) => args_str?.Length > 0 ? args_str[0] : String.Empty, + local_name, use_cfo } }; [Theory] - [MemberData(nameof(GettersTestData), parameters : false)] - [MemberData(nameof(GettersTestData), parameters : true)] - public async Task PropertyGettersOnObjectsTest(string eval_fn, string method_name, int line, int col, string cfo_fn, Func get_args_fn, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite( + [MemberData(nameof(GettersTestData), "ptd", false)] + [MemberData(nameof(GettersTestData), "ptd", true)] + [MemberData(nameof (GettersTestData), "swp", false)] + [MemberData(nameof (GettersTestData), "swp", true)] + public async Task PropertyGettersTest(string eval_fn, string method_name, int line, int col, string cfo_fn, Func get_args_fn, string local_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite( "dotnet://debugger-test.dll/debugger-cfo-test.cs", line, col, method_name, $"window.setTimeout(function() {{ {eval_fn} }}, 1);", @@ -590,55 +626,56 @@ namespace DebuggerTests wait_for_event_fn : async(pause_location) => { var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var dt = new DateTime(10, 9, 8, 7, 6, 5); await CheckProps(frame_locals, new { ptd = TObject("DebuggerTests.ClassWithProperties"), - swp = TObject("DebuggerTests.StructWithProperties"), + swp = TObject("DebuggerTests.StructWithProperties") }, "locals#0"); - var ptd = GetAndAssertObjectWithName(frame_locals, "ptd"); + var obj = GetAndAssertObjectWithName(frame_locals, local_name); - var ptd_props = await GetProperties(ptd?["value"] ? ["objectId"]?.Value()); - await CheckProps(ptd_props, new + var dt = new DateTime(4, 5, 6, 7, 8, 9); + var obj_props = await GetProperties(obj?["value"] ? ["objectId"]?.Value()); + await CheckProps(obj_props, new { - Int = TGetter("Int"), - String = TGetter("String"), - DT = TGetter("DT"), - IntArray = TGetter("IntArray"), - DTArray = TGetter("DTArray") - }, "ptd", num_fields : 7); + V = TNumber(0xDEADBEEF), + Int = TGetter("Int"), + String = TGetter("String"), + DT = TGetter("DT"), + IntArray = TGetter("IntArray"), + DTArray = TGetter("DTArray"), + StringField = TString(null), + + // Auto properties show w/o getters, because they have + // a backing field + DTAutoProperty = TValueType("System.DateTime", dt.ToString()) + }, local_name); // Automatic properties don't have invokable getters, because we can get their // value from the backing field directly { - dt = new DateTime(4, 5, 6, 7, 8, 9); - var dt_auto_props = await GetObjectOnLocals(ptd_props, "DTAutoProperty"); - await CheckDateTime(ptd_props, "DTAutoProperty", dt); + var dt_auto_props = await GetObjectOnLocals(obj_props, "DTAutoProperty"); + await CheckDateTime(obj_props, "DTAutoProperty", dt); } // Invoke getters, and check values - var res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "Int" })); - Assert.True(res.IsOk, $"InvokeGetter failed with : {res}"); - await CheckValue(res.Value["result"], JObject.FromObject(new { type = "number", value = 5 }), "ptd.Int"); + dt = new DateTime(3, 4, 5, 6, 7, 8); + var res = await InvokeGetter(obj, get_args_fn(new [] { "Int" }), cfo_fn); + await CheckValue(res.Value["result"], JObject.FromObject(new { type = "number", value = (0xDEADBEEF + (uint) dt.Month) }), $"{local_name}.Int"); - res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "String" })); - Assert.True(res.IsOk, $"InvokeGetter failed with : {res}"); - await CheckValue(res.Value["result"], JObject.FromObject(new { type = "string", value = "foobar" }), "ptd.String"); + res = await InvokeGetter(obj, get_args_fn(new [] { "String" }), cfo_fn); + await CheckValue(res.Value["result"], JObject.FromObject(new { type = "string", value = $"String property, V: 0xDEADBEEF" }), $"{local_name}.String"); - dt = new DateTime(3, 4, 5, 6, 7, 8); - res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "DT" })); - Assert.True(res.IsOk, $"InvokeGetter failed with : {res}"); - await CheckValue(res.Value["result"], TValueType("System.DateTime", dt.ToString()), "ptd.DT"); + res = await InvokeGetter(obj, get_args_fn(new [] { "DT" }), cfo_fn); + await CheckValue(res.Value["result"], TValueType("System.DateTime", dt.ToString()), $"{local_name}.DT"); await CheckDateTimeValue(res.Value["result"], dt); // Check arrays through getters - res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "IntArray" })); - Assert.True(res.IsOk, $"InvokeGetter failed with : {res}"); - await CheckValue(res.Value["result"], TArray("int[]", 2), "ptd.IntArray"); + res = await InvokeGetter(obj, get_args_fn(new [] { "IntArray" }), cfo_fn); + await CheckValue(res.Value["result"], TArray("int[]", 2), $"{local_name}.IntArray"); { var arr_elems = await GetProperties(res.Value["result"] ? ["objectId"]?.Value()); var exp_elems = new [] @@ -647,12 +684,11 @@ namespace DebuggerTests TNumber(20) }; - await CheckProps(arr_elems, exp_elems, "ptd.IntArray"); + await CheckProps(arr_elems, exp_elems, $"{local_name}.IntArray"); } - res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "DTArray" })); - Assert.True(res.IsOk, $"InvokeGetter failed with : {res}"); - await CheckValue(res.Value["result"], TArray("System.DateTime[]", 2), "ptd.DTArray"); + res = await InvokeGetter(obj, get_args_fn(new [] { "DTArray" }), cfo_fn); + await CheckValue(res.Value["result"], TArray("System.DateTime[]", 2), $"{local_name}.DTArray"); { var dt0 = new DateTime(6, 7, 8, 9, 10, 11); var dt1 = new DateTime(1, 2, 3, 4, 5, 6); @@ -664,44 +700,20 @@ namespace DebuggerTests TValueType("System.DateTime", dt1.ToString()), }; - await CheckProps(arr_elems, exp_elems, "ptd.DTArray"); - } - }); + await CheckProps(arr_elems, exp_elems, $"{local_name}.DTArray"); - [Theory] - [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "MoveNext", 38, 12)] - [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "PropertyGettersTest", 30, 12)] - public async Task PropertyGettersOnStructsTest(string eval_fn, string method_name, int line, int col) => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-cfo-test.cs", line, col, - method_name, - $"window.setTimeout(function() {{ {eval_fn} }}, 1);", - wait_for_event_fn : async(pause_location) => - { - var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - await CheckProps(frame_locals, new - { - ptd = TObject("DebuggerTests.ClassWithProperties"), - swp = TObject("DebuggerTests.StructWithProperties"), - }, "locals#0"); - - var swp = GetAndAssertObjectWithName(frame_locals, "swp"); - - var swp_props = await GetProperties(swp?["value"] ? ["objectId"]?.Value()); - await CheckProps(swp_props, new - { - Int = TSymbol("int { get; }"), - String = TSymbol("string { get; }"), - DT = TSymbol("System.DateTime { get; }"), - IntArray = TSymbol("int[] { get; }"), - DTArray = TSymbol("System.DateTime[] { get; }") - }, "swp"); + res = await InvokeGetter(arr_elems[0], "Date"); + await CheckDateTimeValue(res.Value["result"], dt0.Date); + } }); [Theory] - [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, false)] + [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 38, 12, true)] + [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 38, 12, false)] [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, true)] - [InlineData("invoke_getters_js_test ();", "/other.js", 29, 1, false)] - [InlineData("invoke_getters_js_test ();", "/other.js", 29, 1, true)] + [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, false)] + [InlineData("invoke_getters_js_test ();", "/other.js", 30, 1, false)] + [InlineData("invoke_getters_js_test ();", "/other.js", 30, 1, true)] public async Task CheckAccessorsOnObjectsWithCFO(string eval_fn, string bp_loc, int line, int col, bool roundtrip) { await RunCallFunctionOn( @@ -754,14 +766,126 @@ namespace DebuggerTests } } - async Task InvokeGetter(JToken obj, string fn, object arguments) => await ctx.cli.SendCommand( - "Runtime.callFunctionOn", - JObject.FromObject(new + public static TheoryData NegativeTestsData(bool use_cfo = false) => new TheoryData + { { "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:MethodForNegativeTests', null);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 45, 12, use_cfo }, + { "negative_cfo_test ();", "/other.js", 62, 1, use_cfo } + }; + + [Theory] + [MemberData(nameof(NegativeTestsData), false)] + public async Task RunOnInvalidCfoId(string eval_fn, string bp_loc, int line, int col, bool use_cfo) => await RunCallFunctionOn( + eval_fn, "function() { return this; }", "ptd", + bp_loc, line, col, + test_fn : async(cfo_result) => + { + var ptd_id = cfo_result.Value?["result"] ? ["objectId"]?.Value(); + + var cfo_args = JObject.FromObject(new + { + functionDeclaration = "function () { return 0; }", + objectId = ptd_id + "_invalid" + }); + + var res = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token); + Assert.True(res.IsErr); + }); + + [Theory] + [MemberData(nameof(NegativeTestsData), false)] + public async Task RunOnInvalidThirdSegmentOfObjectId(string eval_fn, string bp_loc, int line, int col, bool use_cfo) + { + 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); + ctx.UseCallFunctionOnBeforeGetProperties = use_cfo; + await SetBreakpoint(bp_loc, line, col); + + // callFunctionOn + var eval_expr = $"window.setTimeout(function() {{ {eval_fn} }}, 1);"; + var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token); + var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE); + + var frame_locals = await GetProperties(pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value()); + var ptd = GetAndAssertObjectWithName(frame_locals, "ptd"); + var ptd_id = ptd["value"]["objectId"].Value(); + + var cfo_args = JObject.FromObject(new + { + functionDeclaration = "function () { return 0; }", + objectId = ptd_id + "_invalid" + }); + + var res = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token); + Assert.True(res.IsErr); + }); + } + + [Theory] + [MemberData(nameof(NegativeTestsData), false)] + [MemberData(nameof(NegativeTestsData), true)] + public async Task InvalidPropertyGetters(string eval_fn, string bp_loc, int line, int col, bool use_cfo) + { + 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 SetBreakpoint(bp_loc, line, col); + ctx.UseCallFunctionOnBeforeGetProperties = use_cfo; + + // callFunctionOn + var eval_expr = $"window.setTimeout(function() {{ {eval_fn} }}, 1);"; + await SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr })); + var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE); + + var frame_locals = await GetProperties(pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value()); + var ptd = GetAndAssertObjectWithName(frame_locals, "ptd"); + var ptd_id = ptd["value"]["objectId"].Value(); + + var invalid_args = new object[] { "NonExistant", String.Empty, null, 12310 }; + foreach (var invalid_arg in invalid_args) + { + var getter_res = await InvokeGetter(JObject.FromObject(new { value = new { objectId = ptd_id } }), invalid_arg); + AssertEqual("undefined", getter_res.Value["result"] ? ["type"]?.ToString(), $"Expected to get undefined result for non-existant accessor - {invalid_arg}"); + } + }); + } + + [Theory] + [MemberData(nameof(NegativeTestsData), false)] + public async Task ReturnNullFromCFO(string eval_fn, string bp_loc, int line, int col, bool use_cfo) => await RunCallFunctionOn( + eval_fn, "function() { return this; }", "ptd", + bp_loc, line, col, + test_fn : async(result) => { - functionDeclaration = fn, - objectId = obj["value"] ? ["objectId"]?.Value(), - arguments = new [] { new { value = arguments } } - }), ctx.token); + var is_js = bp_loc.EndsWith(".js"); + var ptd = JObject.FromObject(new { value = new { objectId = result.Value?["result"] ? ["objectId"]?.Value() } }); + + var null_value_json = JObject.Parse("{ 'type': 'object', 'subtype': 'null', 'value': null }"); + foreach (var returnByValue in new bool?[] { null, false, true }) + { + var res = await InvokeGetter(ptd, "StringField", returnByValue : returnByValue); + if (is_js) + { + // In js case, it doesn't know the className, so the result looks slightly different + Assert.True( + JObject.DeepEquals(res.Value["result"], null_value_json), + $"[StringField#returnByValue = {returnByValue}] Json didn't match. Actual: {res.Value ["result"]} vs {null_value_json}"); + } + else + { + await CheckValue(res.Value["result"], TString(null), "StringField"); + } + } + }); /* * 1. runs `Runtime.callFunctionOn` on the objectId, diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs index e121870..2a62a67 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs @@ -534,12 +534,10 @@ namespace DebuggerTests var complex = GetAndAssertObjectWithName(locals, "complex"); // try to deref the non-pointer object, as a pointer - var props = await GetProperties(complex["value"]["objectId"].Value().Replace(":object:", ":pointer:")); - Assert.Empty(props.Values()); + await GetProperties(complex["value"]["objectId"].Value().Replace(":object:", ":pointer:"), expect_ok: false); // try to deref an invalid pointer id - props = await GetProperties("dotnet:pointer:123897"); - Assert.Empty(props.Values()); + await GetProperties("dotnet:pointer:123897", expect_ok: false); }); async Task CheckArrayElements(JToken array, JToken[] exp_elems) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs b/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs index cac5e73..ac85709 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Microsoft.WebAssembly.Diagnostics; using Xunit; @@ -301,6 +302,7 @@ namespace DebuggerTests var val = l["value"]; Assert.Equal("number", val["type"]?.Value()); Assert.Equal(value, val["value"].Value()); + Assert.Equal(value.ToString(), val["description"].Value().ToString()); return; } Assert.True(false, $"Could not find variable '{name}'"); @@ -308,32 +310,14 @@ namespace DebuggerTests internal void CheckString(JToken locals, string name, string value) { - foreach (var l in locals) - { - if (name != l["name"]?.Value()) - continue; - var val = l["value"]; - if (value == null) - { - Assert.Equal("object", val["type"]?.Value()); - Assert.Equal("null", val["subtype"]?.Value()); - } - else - { - Assert.Equal("string", val["type"]?.Value()); - Assert.Equal(value, val["value"]?.Value()); - } - return; - } - Assert.True(false, $"Could not find variable '{name}'"); + var l = GetAndAssertObjectWithName(locals, name); + CheckValue(l["value"], TString(value), name).Wait(); } internal JToken CheckSymbol(JToken locals, string name, string value) { var l = GetAndAssertObjectWithName(locals, name); - var val = l["value"]; - Assert.Equal("symbol", val["type"]?.Value()); - Assert.Equal(value, val["value"]?.Value()); + CheckValue(l["value"], TSymbol(value), name).Wait(); return l; } @@ -341,14 +325,8 @@ namespace DebuggerTests { var l = GetAndAssertObjectWithName(locals, name); var val = l["value"]; - Assert.Equal("object", val["type"]?.Value()); + CheckValue(val, TObject(class_name, is_null: is_null), name).Wait(); Assert.True(val["isValueType"] == null || !val["isValueType"].Value()); - Assert.Equal(class_name, val["className"]?.Value()); - - var has_null_subtype = val["subtype"] != null && val["subtype"]?.Value() == "null"; - Assert.Equal(is_null, has_null_subtype); - if (subtype != null) - Assert.Equal(subtype, val["subtype"]?.Value()); return l; } @@ -368,31 +346,34 @@ namespace DebuggerTests internal async Task CheckDateTimeValue(JToken value, DateTime expected) { - AssertEqual("System.DateTime", value["className"]?.Value(), "className"); - AssertEqual(expected.ToString(), value["description"]?.Value(), "description"); - - var members = await GetProperties(value["objectId"]?.Value()); + await CheckDateTimeMembers(value, expected); - // not checking everything - CheckNumber(members, "Year", expected.Year); - CheckNumber(members, "Month", expected.Month); - CheckNumber(members, "Day", expected.Day); - CheckNumber(members, "Hour", expected.Hour); - CheckNumber(members, "Minute", expected.Minute); - CheckNumber(members, "Second", expected.Second); + var res = await InvokeGetter(JObject.FromObject(new { value = value }), "Date"); + await CheckDateTimeMembers(res.Value["result"], expected.Date); // FIXME: check some float properties too + + async Task CheckDateTimeMembers(JToken v, DateTime exp_dt) + { + AssertEqual("System.DateTime", v["className"]?.Value(), "className"); + AssertEqual(exp_dt.ToString(), v["description"]?.Value(), "description"); + + var members = await GetProperties(v["objectId"]?.Value()); + + // not checking everything + CheckNumber(members, "Year", exp_dt.Year); + CheckNumber(members, "Month", exp_dt.Month); + CheckNumber(members, "Day", exp_dt.Day); + CheckNumber(members, "Hour", exp_dt.Hour); + CheckNumber(members, "Minute", exp_dt.Minute); + CheckNumber(members, "Second", exp_dt.Second); + } } internal JToken CheckBool(JToken locals, string name, bool expected) { var l = GetAndAssertObjectWithName(locals, name); - var val = l["value"]; - Assert.Equal("boolean", val["type"]?.Value()); - if (val["value"] == null) - Assert.True(false, "expected bool value not found for variable named {name}"); - Assert.Equal(expected, val["value"]?.Value()); - + CheckValue(l["value"], TBool(expected), name).Wait(); return l; } @@ -405,41 +386,21 @@ namespace DebuggerTests internal JToken CheckValueType(JToken locals, string name, string class_name) { var l = GetAndAssertObjectWithName(locals, name); - var val = l["value"]; - Assert.Equal("object", val["type"]?.Value()); - Assert.True(val["isValueType"] != null && val["isValueType"].Value()); - Assert.Equal(class_name, val["className"]?.Value()); + CheckValue(l["value"], TValueType(class_name), name).Wait(); return l; } internal JToken CheckEnum(JToken locals, string name, string class_name, string descr) { var l = GetAndAssertObjectWithName(locals, name); - var val = l["value"]; - Assert.Equal("object", val["type"]?.Value()); - Assert.True(val["isEnum"] != null && val["isEnum"].Value()); - Assert.Equal(class_name, val["className"]?.Value()); - Assert.Equal(descr, val["description"]?.Value()); + CheckValue(l["value"], TEnum(class_name, descr), name).Wait(); return l; } - internal void CheckArray(JToken locals, string name, string class_name) - { - foreach (var l in locals) - { - if (name != l["name"]?.Value()) - continue; - - var val = l["value"]; - Assert.Equal("object", val["type"]?.Value()); - Assert.Equal("array", val["subtype"]?.Value()); - Assert.Equal(class_name, val["className"]?.Value()); - - //FIXME: elements? - return; - } - Assert.True(false, $"Could not find variable '{name}'"); - } + internal void CheckArray(JToken locals, string name, string class_name, int length) + => CheckValue( + GetAndAssertObjectWithName(locals, name)["value"], + TArray(class_name, length), name).Wait(); internal JToken GetAndAssertObjectWithName(JToken obj, string name) { @@ -482,6 +443,23 @@ namespace DebuggerTests return wait_res; } + internal async Task InvokeGetter(JToken obj, object arguments, string fn = "function(e){return this[e]}", bool expect_ok = true, bool? returnByValue = null) + { + var req = JObject.FromObject(new + { + functionDeclaration = fn, + objectId = obj["value"]?["objectId"]?.Value(), + arguments = new[] { new { value = arguments } } + }); + if (returnByValue != null) + req["returnByValue"] = returnByValue.Value; + + var res = await ctx.cli.SendCommand("Runtime.callFunctionOn", req, ctx.token); + Assert.True(expect_ok == res.IsOk, $"InvokeGetter failed for {req} with {res}"); + + return res; + } + internal async Task StepAndCheck(StepKind kind, string script_loc, int line, int column, string function_name, Func wait_for_event_fn = null, Action locals_fn = null, int times = 1) { @@ -796,7 +774,7 @@ namespace DebuggerTests } /* @fn_args is for use with `Runtime.callFunctionOn` only */ - internal async Task GetProperties(string id, JToken fn_args = null) + internal async Task GetProperties(string id, JToken fn_args = null, bool expect_ok = true) { if (ctx.UseCallFunctionOnBeforeGetProperties && !id.StartsWith("dotnet:scope:")) { @@ -810,7 +788,9 @@ namespace DebuggerTests cfo_args["arguments"] = fn_args; var result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token); - AssertEqual(true, result.IsOk, $"Runtime.getProperties failed for {cfo_args.ToString ()}, with Result: {result}"); + AssertEqual(expect_ok, result.IsOk, $"Runtime.getProperties returned {result.IsOk} instead of {expect_ok}, for {cfo_args.ToString ()}, with Result: {result}"); + if (!result.IsOk) + return null; id = result.Value["result"] ? ["objectId"]?.Value(); } @@ -820,8 +800,9 @@ namespace DebuggerTests }); var frame_props = await ctx.cli.SendCommand("Runtime.getProperties", get_prop_req, ctx.token); + AssertEqual(expect_ok, frame_props.IsOk, $"Runtime.getProperties returned {frame_props.IsOk} instead of {expect_ok}, for {get_prop_req}, with Result: {frame_props}"); if (!frame_props.IsOk) - Assert.True(false, $"Runtime.getProperties failed for {get_prop_req.ToString ()}, with Result: {frame_props}"); + return null; var locals = frame_props.Value["result"]; // FIXME: Should be done when generating the list in library_mono.js, but not sure yet @@ -927,11 +908,14 @@ namespace DebuggerTests internal static JObject TString(string value) => value == null ? TObject("string", is_null : true) : - JObject.FromObject(new { type = "string", value = @value, description = @value }); + JObject.FromObject(new { type = "string", value = @value }); internal static JObject TNumber(int value) => JObject.FromObject(new { type = "number", value = @value.ToString(), description = value.ToString() }); + internal static JObject TNumber(uint value) => + JObject.FromObject(new { type = "number", value = @value.ToString(), description = value.ToString() }); + internal static JObject TValueType(string className, string description = null, object members = null) => JObject.FromObject(new { type = "object", isValueType = true, className = className, description = description ?? className }); @@ -987,6 +971,65 @@ namespace DebuggerTests } } + class DotnetObjectId + { + public string Scheme { get; } + public string Value { get; } + + JObject value_json; + public JObject ValueAsJson + { + get + { + if (value_json == null) + { + try + { + value_json = JObject.Parse(Value); + } + catch (JsonReaderException) { } + } + + return value_json; + } + } + + public static bool TryParse(JToken jToken, out DotnetObjectId objectId) => TryParse(jToken?.Value(), out objectId); + + public static bool TryParse(string id, out DotnetObjectId objectId) + { + objectId = null; + if (id == null) + { + return false; + } + + if (!id.StartsWith("dotnet:")) + { + return false; + } + + var parts = id.Split(":", 3); + + if (parts.Length < 3) + { + return false; + } + + objectId = new DotnetObjectId(parts[1], parts[2]); + + return true; + } + + public DotnetObjectId(string scheme, string value) + { + Scheme = scheme; + Value = value; + } + + public override string ToString() => $"dotnet:{Scheme}:{Value}"; + } + enum StepKind { Into, diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs index 03964dd..5ba31d8 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs @@ -372,14 +372,14 @@ namespace DebuggerTests CheckObject(locals, "list", "System.Collections.Generic.Dictionary"); CheckObject(locals, "list_null", "System.Collections.Generic.Dictionary", is_null : true); - CheckArray(locals, "list_arr", "System.Collections.Generic.Dictionary[]"); + CheckArray(locals, "list_arr", "System.Collections.Generic.Dictionary[]", 1); CheckObject(locals, "list_arr_null", "System.Collections.Generic.Dictionary[]", is_null : true); // Unused locals CheckObject(locals, "list_unused", "System.Collections.Generic.Dictionary"); CheckObject(locals, "list_null_unused", "System.Collections.Generic.Dictionary", is_null : true); - CheckObject(locals, "list_arr_unused", "System.Collections.Generic.Dictionary[]"); + CheckArray(locals, "list_arr_unused", "System.Collections.Generic.Dictionary[]", 1); CheckObject(locals, "list_arr_null_unused", "System.Collections.Generic.Dictionary[]", is_null : true); } ); @@ -638,9 +638,11 @@ namespace DebuggerTests CheckValueType(this_props, "SimpleStructProperty", "Math.SimpleStruct"); var ss_props = await GetObjectOnLocals(this_props, "SimpleStructProperty"); - Assert.Equal(2, ss_props.Count()); - CheckValueType(ss_props, "dt", "System.DateTime"); - CheckValueType(ss_props, "gs", "Math.GenericStruct"); + var dt = new DateTime(2020, 1, 2, 3, 4, 5); + await CheckProps(ss_props, new { + dt = TValueType("System.DateTime", dt.ToString()), + gs = TValueType("Math.GenericStruct") + }, "ss_props"); await CheckDateTime(ss_props, "dt", new DateTime(2020, 1, 2, 3, 4, 5)); @@ -829,7 +831,7 @@ namespace DebuggerTests CheckObject(locals, "this", "Math.NestedInMath"); //FIXME: check fields CheckValueType(locals, "ss", "Math.SimpleStruct"); - CheckArray(locals, "ss_arr", "Math.SimpleStruct[]"); + CheckArray(locals, "ss_arr", "Math.SimpleStruct[]", 0); // TODO: struct fields } ); @@ -863,29 +865,40 @@ namespace DebuggerTests var pause_location = await EvaluateAndCheck( "window.setTimeout(function() { invoke_method_with_structs(); }, 1);", - debugger_test_loc, 22, 8, "MethodWithLocalStructs", - locals_fn: (locals) => - { - Assert.Equal(3, locals.Count()); + debugger_test_loc, 22, 8, "MethodWithLocalStructs"); - CheckValueType(locals, "ss_local", "DebuggerTests.ValueTypesTest.SimpleStruct"); - CheckValueType(locals, "gs_local", "DebuggerTests.ValueTypesTest.GenericStruct"); - CheckObject(locals, "vt_local", "DebuggerTests.ValueTypesTest"); - } - ); + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + await CheckProps(locals, new + { + ss_local = TValueType("DebuggerTests.ValueTypesTest.SimpleStruct"), + gs_local = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), + vt_local = TObject("DebuggerTests.ValueTypesTest") + }, "locals"); var dt = new DateTime(2021, 2, 3, 4, 6, 7); + var vt_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "vt_local"); + Assert.Equal(5, vt_local_props.Count()); + + CheckString(vt_local_props, "StringField", "string#0"); + CheckValueType(vt_local_props, "SimpleStructField", "DebuggerTests.ValueTypesTest.SimpleStruct"); + CheckValueType(vt_local_props, "SimpleStructProperty", "DebuggerTests.ValueTypesTest.SimpleStruct"); + await CheckDateTime(vt_local_props, "DT", new DateTime(2020, 1, 2, 3, 4, 5)); + CheckEnum(vt_local_props, "RGB", "DebuggerTests.RGB", "Blue"); + // Check ss_local's properties var ss_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_local"); await CheckProps(ss_local_props, new { + V = TGetter("V"), str_member = TString("set in MethodWithLocalStructs#SimpleStruct#str_member"), - dt = TValueType("System.DateTime", dt.ToString()), - gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), - Kind = TEnum("System.DateTimeKind", "Utc") + dt = TValueType("System.DateTime", dt.ToString()), + gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), + Kind = TEnum("System.DateTimeKind", "Utc") }, "ss_local"); { + var gres = await InvokeGetter(GetAndAssertObjectWithName(locals, "ss_local"), "V"); + await CheckValue(gres.Value["result"], TNumber(0xDEADBEEF + 2), $"ss_local#V"); // Check ss_local.dt await CheckDateTime(ss_local_props, "dt", dt); @@ -905,43 +918,30 @@ namespace DebuggerTests }, "gs_local"); // Check vt_local's properties - var vt_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "vt_local"); - Assert.Equal(5, vt_local_props.Count()); - CheckString(vt_local_props, "StringField", "string#0"); - CheckValueType(vt_local_props, "SimpleStructField", "DebuggerTests.ValueTypesTest.SimpleStruct"); - CheckValueType(vt_local_props, "SimpleStructProperty", "DebuggerTests.ValueTypesTest.SimpleStruct"); - await CheckDateTime(vt_local_props, "DT", new DateTime(2020, 1, 2, 3, 4, 5)); - CheckEnum(vt_local_props, "RGB", "DebuggerTests.RGB", "Blue"); + var exp = new[] + { + ("SimpleStructProperty", 2, "Utc"), + ("SimpleStructField", 5, "Local") + }; + foreach (var (name, bias, dt_kind) in exp) { - // SimpleStructProperty - dt = new DateTime(2022, 3, 4, 5, 7, 8); - var ssp_props = await CompareObjectPropertiesFor(vt_local_props, "SimpleStructProperty", + dt = new DateTime(2020 + bias, 1 + bias, 2 + bias, 3 + bias, 5 + bias, 6 + bias); + var ssp_props = await CompareObjectPropertiesFor(vt_local_props, name, new { - str_member = TString("SimpleStructProperty#string#0#SimpleStruct#str_member"), - dt = TValueType("System.DateTime", dt.ToString()), - gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), - Kind = TEnum("System.DateTimeKind", "Utc") + V = TGetter("V"), + str_member = TString($"{name}#string#0#SimpleStruct#str_member"), + dt = TValueType("System.DateTime", dt.ToString()), + gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), + Kind = TEnum("System.DateTimeKind", dt_kind) }, - label: "vt_local_props.SimpleStructProperty"); + label: $"vt_local_props.{name}"); await CheckDateTime(ssp_props, "dt", dt); - - // SimpleStructField - dt = new DateTime(2025, 6, 7, 8, 10, 11); - var ssf_props = await CompareObjectPropertiesFor(vt_local_props, "SimpleStructField", - new - { - str_member = TString("SimpleStructField#string#0#SimpleStruct#str_member"), - dt = TValueType("System.DateTime", dt.ToString()), - gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), - Kind = TEnum("System.DateTimeKind", "Local") - }, - label: "vt_local_props.SimpleStructField"); - - await CheckDateTime(ssf_props, "dt", dt); + var gres = await InvokeGetter(GetAndAssertObjectWithName(vt_local_props, name), "V"); + await CheckValue(gres.Value["result"], TNumber(0xDEADBEEF + (uint) dt.Month), $"{name}#V"); } // FIXME: check ss_local.gs.List's members @@ -968,20 +968,19 @@ namespace DebuggerTests var pause_location = await EvaluateAndCheck( "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:TestStructsAsMethodArgs'); }, 1);", - debugger_test_loc, 34, 12, "MethodWithStructArgs", - locals_fn: (locals) => - { - Assert.Equal(3, locals.Count()); - - CheckString(locals, "label", "TestStructsAsMethodArgs#label"); - CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct"); - CheckNumber(locals, "x", 3); - } - ); + debugger_test_loc, 34, 12, "MethodWithStructArgs"); + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + { + Assert.Equal(3, locals.Count()); + CheckString(locals, "label", "TestStructsAsMethodArgs#label"); + CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct"); + CheckNumber(locals, "x", 3); + } var dt = new DateTime(2025, 6, 7, 8, 10, 11); var ss_local_as_ss_arg = new { + V = TGetter("V"), str_member = TString("ss_local#SimpleStruct#string#0#SimpleStruct#str_member"), dt = TValueType("System.DateTime", dt.ToString()), gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), @@ -998,6 +997,9 @@ namespace DebuggerTests var ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_arg"); await CheckProps(ss_arg_props, ss_local_as_ss_arg, "ss_arg"); + var res = await InvokeGetter(GetAndAssertObjectWithName(locals, "ss_arg"), "V"); + await CheckValue(res.Value["result"], TNumber(0xDEADBEEF + (uint) dt.Month), "ss_arg#V"); + { // Check ss_local.dt await CheckDateTime(ss_arg_props, "dt", dt); @@ -1007,19 +1009,19 @@ namespace DebuggerTests } pause_location = await StepAndCheck(StepKind.Over, debugger_test_loc, 38, 8, "MethodWithStructArgs", times : 4, - locals_fn: (locals) => - { - Assert.Equal(3, locals.Count()); - - CheckString(locals, "label", "TestStructsAsMethodArgs#label"); - CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct"); - CheckNumber(locals, "x", 3); + locals_fn: (l) => { /* non-null to make sure that locals get fetched */ }); + locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + { + Assert.Equal(3, locals.Count()); - } - ); + CheckString(locals, "label", "TestStructsAsMethodArgs#label"); + CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct"); + CheckNumber(locals, "x", 3); + } var ss_arg_updated = new { + V = TGetter("V"), str_member = TString("ValueTypesTest#MethodWithStructArgs#updated#ss_arg#str_member"), dt = TValueType("System.DateTime", dt.ToString()), gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), @@ -1027,7 +1029,10 @@ namespace DebuggerTests }; ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_arg"); - await CheckProps(ss_arg_props, ss_arg_updated, "ss_ar"); + await CheckProps(ss_arg_props, ss_arg_updated, "ss_arg"); + + res = await InvokeGetter(GetAndAssertObjectWithName(locals, "ss_arg"), "V"); + await CheckValue(res.Value["result"], TNumber(0xDEADBEEF + (uint) dt.Month), "ss_arg#V"); { // Check ss_local.gs @@ -1059,7 +1064,7 @@ namespace DebuggerTests pause_location = await StepAndCheck(StepKind.Over, debugger_test_loc, 28, 12, "TestStructsAsMethodArgs", times : 2, locals_fn: (l) => { /* non-null to make sure that locals get fetched */ }); - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); await CheckProps(locals, new { ss_local = TValueType("DebuggerTests.ValueTypesTest.SimpleStruct"), @@ -1092,7 +1097,7 @@ namespace DebuggerTests ctx = new DebugTestContext(cli, insp, token, scripts); var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs"; - var lines = new [] { 202, 205 }; + var lines = new [] { 203, 206 }; await SetBreakpoint(debugger_test_loc, lines[0], 12); await SetBreakpoint(debugger_test_loc, lines[1], 12); @@ -1100,16 +1105,14 @@ namespace DebuggerTests "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:MethodUpdatingValueTypeMembers'); }, 1);", debugger_test_loc, lines[0], 12, "MethodUpdatingValueTypeMembers"); - var dt = new DateTime(1, 2, 3, 4, 5, 6); - await CheckLocals(pause_location, dt); + await CheckLocals(pause_location, new DateTime(1, 2, 3, 4, 5, 6), new DateTime(4, 5, 6, 7, 8, 9)); // Resume - dt = new DateTime(9, 8, 7, 6, 5, 4); pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", debugger_test_loc, lines[1], 12, "MethodUpdatingValueTypeMembers"); - await CheckLocals(pause_location, dt); + await CheckLocals(pause_location, new DateTime(9, 8, 7, 6, 5, 4), new DateTime(5, 1, 3, 7, 9, 10)); }); - async Task CheckLocals(JToken pause_location, DateTime dt) + async Task CheckLocals(JToken pause_location, DateTime obj_dt, DateTime vt_dt) { var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); await CheckProps(locals, new @@ -1122,20 +1125,20 @@ namespace DebuggerTests { await CheckProps(obj_props, new { - DT = TValueType("System.DateTime", dt.ToString()) + DT = TValueType("System.DateTime", obj_dt.ToString()) }, "locals#obj.DT", num_fields : 5); - await CheckDateTime(obj_props, "DT", dt); + await CheckDateTime(obj_props, "DT", obj_dt); } - var vt_props = await GetObjectOnLocals(locals, "obj"); + var vt_props = await GetObjectOnLocals(locals, "vt"); { await CheckProps(vt_props, new { - DT = TValueType("System.DateTime", dt.ToString()) + DT = TValueType("System.DateTime", vt_dt.ToString()) }, "locals#obj.DT", num_fields : 5); - await CheckDateTime(vt_props, "DT", dt); + await CheckDateTime(vt_props, "DT", vt_dt); } } } @@ -1153,7 +1156,7 @@ namespace DebuggerTests ctx = new DebugTestContext(cli, insp, token, scripts); var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs"; - var lines = new [] { 211, 213 }; + var lines = new [] { 212, 214 }; await SetBreakpoint(debugger_test_loc, lines[0], 12); await SetBreakpoint(debugger_test_loc, lines[1], 12); @@ -1186,7 +1189,7 @@ namespace DebuggerTests ctx = new DebugTestContext(cli, insp, token, scripts); var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs"; - var lines = new [] { 222, 224 }; + var lines = new [] { 223, 225 }; await SetBreakpoint(debugger_test_loc, lines[0], 12); await SetBreakpoint(debugger_test_loc, lines[1], 12); @@ -1259,13 +1262,17 @@ namespace DebuggerTests var ss_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_local"); await CheckProps(ss_local_props, new { + V = TGetter("V"), str_member = TString("set in MethodWithLocalStructsStaticAsync#SimpleStruct#str_member"), - dt = TValueType("System.DateTime", dt.ToString()), - gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), - Kind = TEnum("System.DateTimeKind", "Utc") + dt = TValueType("System.DateTime", dt.ToString()), + gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), + Kind = TEnum("System.DateTimeKind", "Utc") }, "ss_local"); { + var gres = await InvokeGetter(GetAndAssertObjectWithName(locals, "ss_local"), "V"); + await CheckValue(gres.Value["result"], TNumber(0xDEADBEEF + 2), $"ss_local#V"); + // Check ss_local.dt await CheckDateTime(ss_local_props, "dt", dt); @@ -1294,10 +1301,10 @@ namespace DebuggerTests } [Theory] - [InlineData(134, 12, "MethodWithLocalsForToStringTest", false, false)] - [InlineData(144, 12, "MethodWithArgumentsForToStringTest", true, false)] - [InlineData(189, 12, "MethodWithArgumentsForToStringTestAsync", true, true)] - [InlineData(179, 12, "MethodWithArgumentsForToStringTestAsync", false, true)] + [InlineData(135, 12, "MethodWithLocalsForToStringTest", false, false)] + [InlineData(145, 12, "MethodWithArgumentsForToStringTest", true, false)] + [InlineData(190, 12, "MethodWithArgumentsForToStringTestAsync", true, true)] + [InlineData(180, 12, "MethodWithArgumentsForToStringTestAsync", false, true)] public async Task InspectLocalsForToStringDescriptions(int line, int col, string method_name, bool call_other, bool invoke_async) { var insp = new Inspector(); @@ -1478,6 +1485,43 @@ namespace DebuggerTests }); } + [Fact] + public async Task InvalidValueTypeData() + { + await CheckInspectLocalsAtBreakpointSite( + "dotnet://debugger-test.dll/debugger-test.cs", 85, 8, + "OuterMethod", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] Math:OuterMethod'); })", + wait_for_event_fn : async(pause_location) => + { + var new_id = await CreateNewId(@"MONO._new_or_add_id_props ({ scheme: 'valuetype', idArgs: { containerId: 1 }, props: { klass: 3, value64: 4 }});"); + await _invoke_getter(new_id, "NonExistant", expect_ok : false); + + new_id = await CreateNewId(@"MONO._new_or_add_id_props ({ scheme: 'valuetype', idArgs: { containerId: 1 }, props: { klass: 3 }});"); + await _invoke_getter(new_id, "NonExistant", expect_ok : false); + + new_id = await CreateNewId(@"MONO._new_or_add_id_props ({ scheme: 'valuetype', idArgs: { containerId: 1 }, props: { klass: 3, value64: 'AA' }});"); + await _invoke_getter(new_id, "NonExistant", expect_ok : false); + }); + + async Task CreateNewId(string expr) + { + var res = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = expr }), ctx.token); + Assert.True(res.IsOk, "Expected Runtime.evaluate to succeed"); + AssertEqual("string", res.Value["result"] ? ["type"]?.Value(), "Expected Runtime.evaluate to return a string type result"); + return res.Value["result"] ? ["value"]?.Value(); + } + + async Task _invoke_getter(string obj_id, string property_name, bool expect_ok) + { + var expr = $"MONO._invoke_getter ('{obj_id}', '{property_name}')"; + var res = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = expr }), ctx.token); + AssertEqual(expect_ok, res.IsOk, "Runtime.evaluate result not as expected for {expr}"); + + return res; + } + } + //TODO add tests covering basic stepping behavior as step in/out/over } -} \ No newline at end of file +} diff --git a/src/mono/wasm/debugger/tests/debugger-cfo-test.cs b/src/mono/wasm/debugger/tests/debugger-cfo-test.cs index ad3b12c..6e88564 100644 --- a/src/mono/wasm/debugger/tests/debugger-cfo-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-cfo-test.cs @@ -26,24 +26,32 @@ namespace DebuggerTests public static void PropertyGettersTest() { - var ptd = new ClassWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9) }; - var swp = new StructWithProperties(); + var ptd = new ClassWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9), V = 0xDEADBEEF }; + var swp = new StructWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9), V = 0xDEADBEEF }; System.Console.WriteLine("break here"); } public static async System.Threading.Tasks.Task PropertyGettersTestAsync() { - var ptd = new ClassWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9) }; - var swp = new StructWithProperties(); + var ptd = new ClassWithProperties { DTAutoProperty = new DateTime (4, 5, 6, 7, 8, 9), V = 0xDEADBEEF }; + var swp = new StructWithProperties { DTAutoProperty = new DateTime (4, 5, 6, 7, 8, 9), V = 0xDEADBEEF }; System.Console.WriteLine("break here"); await System.Threading.Tasks.Task.CompletedTask; } + + public static void MethodForNegativeTests (string value = null) + { + var ptd = new ClassWithProperties { StringField = value }; + var swp = new StructWithProperties { StringField = value }; + Console.WriteLine("break here"); + } } class ClassWithProperties { - public int Int { get { return 5; } } - public string String { get { return "foobar"; } } + public uint V; + public uint Int { get { return V + (uint)DT.Month; } } + public string String { get { return $"String property, V: 0x{V:X}"; } } public DateTime DT { get { return new DateTime(3, 4, 5, 6, 7, 8); } } public int[] IntArray { get { return new int[] { 10, 20 }; } } @@ -54,11 +62,14 @@ namespace DebuggerTests struct StructWithProperties { - public int Int { get { return 5; } } - public string String { get { return "foobar"; } } + public uint V; + public uint Int { get { return V + (uint)DT.Month; } } + public string String { get { return $"String property, V: 0x{V:X}"; } } public DateTime DT { get { return new DateTime(3, 4, 5, 6, 7, 8); } } public int[] IntArray { get { return new int[] { 10, 20 }; } } public DateTime[] DTArray { get { return new DateTime[] { new DateTime(6, 7, 8, 9, 10, 11), new DateTime(1, 2, 3, 4, 5, 6) }; } } + public DateTime DTAutoProperty { get; set; } + public string StringField; } } \ No newline at end of file diff --git a/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs b/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs index 3a4ceea..7c37287 100644 --- a/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs @@ -61,6 +61,7 @@ namespace DebuggerTests public struct SimpleStruct { + public uint V { get { return 0xDEADBEEF + (uint)dt.Month; } set { } } public string str_member; public DateTime dt; public GenericStruct gs; diff --git a/src/mono/wasm/debugger/tests/other.js b/src/mono/wasm/debugger/tests/other.js index a1b121e..9353268 100644 --- a/src/mono/wasm/debugger/tests/other.js +++ b/src/mono/wasm/debugger/tests/other.js @@ -50,3 +50,16 @@ function exceptions_test () { exception_uncaught_test (); } +function negative_cfo_test (str_value = null) { + var ptd = { + get Int () { return 5; }, + get String () { return "foobar"; }, + get DT () { return "dt"; }, + get IntArray () { return [1,2,3]; }, + get DTArray () { return ["dt0", "dt1"]; }, + DTAutoProperty: "dt", + StringField: str_value + }; + console.log (`break here`); + return ptd; +} diff --git a/src/mono/wasm/runtime/library_mono.js b/src/mono/wasm/runtime/library_mono.js index 5e1a177..14e191e 100644 --- a/src/mono/wasm/runtime/library_mono.js +++ b/src/mono/wasm/runtime/library_mono.js @@ -1,6 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +/** + * @typedef WasmId + * @type {object} + * @property {string} idStr - full object id string + * @property {string} scheme - eg, object, valuetype, array .. + * @property {string} value - string part after `dotnet:scheme:` of the id string + * @property {object} o - value parsed as JSON + */ + var MonoSupportLib = { $MONO__postset: 'MONO.export_functions (Module);', $MONO: { @@ -9,6 +18,10 @@ var MonoSupportLib = { _vt_stack: [], mono_wasm_runtime_is_ready : false, mono_wasm_ignore_pdb_load_errors: true, + + /** @type {object.} */ + _id_table: {}, + pump_message: function () { if (!this.mono_background_exec) this.mono_background_exec = Module.cwrap ("mono_background_exec", null); @@ -98,30 +111,19 @@ var MonoSupportLib = { }, _fixup_name_value_objects: function (var_list) { - var out_list = []; - - var _fixup_value = function (value) { - if (value != null && value != undefined) { - var descr = value.description; - if (descr == null || descr == undefined) - value.description = '' + value.value; - } - return value; - }; + let out_list = []; var i = 0; while (i < var_list.length) { - var o = var_list [i]; - var name = o.name; + let o = var_list [i]; + const name = o.name; if (name == null || name == undefined) { i ++; - o.value = _fixup_value(o.value); out_list.push (o); continue; } if (i + 1 < var_list.length) { - _fixup_value(var_list[i + 1].value); o = Object.assign (o, var_list [i + 1]); } @@ -133,8 +135,8 @@ var MonoSupportLib = { }, _filter_automatic_properties: function (props) { - var names_found = {}; - var final_var_list = []; + let names_found = {}; + let final_var_list = []; for (var i in props) { var p = props [i]; @@ -153,46 +155,116 @@ var MonoSupportLib = { return final_var_list; }, - // Given `dotnet:object:foo:bar`, - // returns [ 'dotnet', 'object', 'foo:bar'] - _split_object_id: function (id, delimiter = ':', count = 3) { - if (id === undefined || id == "") - return []; + /** Given `dotnet:object:foo:bar`, + * returns { scheme:'object', value: 'foo:bar' } + * + * Given `dotnet:pointer:{ b: 3 }` + * returns { scheme:'object', value: '{b:3}`, o: {b:3} + * + * @param {string} idStr + * @param {boolean} [throwOnError=false] + * + * @returns {WasmId} + */ + _parse_object_id: function (idStr, throwOnError = false) { + if (idStr === undefined || idStr == "" || !idStr.startsWith ('dotnet:')) { + if (throwOnError) + throw new Error (`Invalid id: ${idStr}`); + + return undefined; + } + + const [, scheme, ...rest] = idStr.split(':'); + let res = { + scheme, + value: rest.join (':'), + idStr, + o: {} + }; + + try { + res.o = JSON.parse(res.value); + // eslint-disable-next-line no-empty + } catch (e) {} - if (delimiter === undefined) delimiter = ':'; - if (count === undefined) count = 3; + return res; + }, + + /** + * @param {WasmId} id + * @returns {object[]} + */ + _get_vt_properties: function (id) { + let entry = this._id_table [id.idStr]; + if (entry !== undefined && entry.members !== undefined) + return entry.members; + + if (!isNaN (id.o.containerId)) + this._get_object_properties (id.o.containerId, true); + else if (!isNaN (id.o.arrayId)) + this._get_array_values (id, Number (id.o.arrayIdx), 1, true); + else + throw new Error (`Invalid valuetype id (${id.idStr}). Can't get properties for it.`); - var var_arr = id.split (delimiter); - var result = var_arr.splice (0, count - 1); + entry = this._get_id_props (id.idStr); + if (entry !== undefined && entry.members !== undefined) + return entry.members; - if (var_arr.length > 0) - result.push (var_arr.join (delimiter)); - return result; + throw new Error (`Unknown valuetype id: ${id.idStr}`); + }, + + /** + * + * @callback GetIdArgsCallback + * @param {object} var + * @param {number} idx + * @returns {object} + */ + + /** + * @param {object[]} vars + * @param {GetIdArgsCallback} getIdArgs + * @returns {object} + */ + _assign_vt_ids: function (vars, getIdArgs) + { + vars.forEach ((v, i) => { + // we might not have a `.value`, like in case of getters which have a `.get` instead + const value = v.value; + if (value === undefined || !value.isValueType) + return; + + if (value.objectId !== undefined) + throw new Error (`Bug: Trying to assign valuetype id, but the var already has one: ${v}`); + + value.objectId = this._new_or_add_id_props ({ scheme: 'valuetype', idArgs: getIdArgs (v, i), props: value._props }); + delete value._props; + }); + + return vars; }, // // @var_list: [ { index: , name: }, .. ] mono_wasm_get_variables: function(scope, var_list) { - if (!this.mono_wasm_get_var_info) - this.mono_wasm_get_var_info = Module.cwrap ("mono_wasm_get_var_info", null, [ 'number', 'number', 'number']); - - this.var_info = []; - var numBytes = var_list.length * Int32Array.BYTES_PER_ELEMENT; - var ptr = Module._malloc(numBytes); - var heapBytes = new Int32Array(Module.HEAP32.buffer, ptr, numBytes); + const numBytes = var_list.length * Int32Array.BYTES_PER_ELEMENT; + const ptr = Module._malloc(numBytes); + let heapBytes = new Int32Array(Module.HEAP32.buffer, ptr, numBytes); for (let i=0; i ({ containerId: this._async_method_objectId, fieldOffset: v.fieldOffset })); - var value = res[i].value; + for (let i in res) { + const res_name = res [i].name; if (this._async_method_objectId != 0) { //Async methods are special in the way that local variables can be lifted to generated class fields //value of "this" comes here either @@ -201,9 +273,6 @@ var MonoSupportLib = { // ALTHOUGH, the name wouldn't have `<>` for method args res [i].name = res_name.substring (1, res_name.indexOf ('>')); } - - if (value.isValueType) - value.objectId = `dotnet:valuetype:${this._async_method_objectId}:${res [i].fieldOffset}`; } else if (res_name === undefined && var_list [i] !== undefined) { // For non-async methods, we just have the var id, but we have the name // from the caller @@ -212,70 +281,49 @@ var MonoSupportLib = { } this._post_process_details(res); - this.var_info = [] - return res; }, - - mono_wasm_get_object_properties: function(objId, expandValueTypes) { - if (!this.mono_wasm_get_object_properties_info) - this.mono_wasm_get_object_properties_info = Module.cwrap ("mono_wasm_get_object_properties", null, [ 'number', 'bool' ]); - - this.var_info = []; - this.mono_wasm_get_object_properties_info (objId, expandValueTypes); - - var res = MONO._filter_automatic_properties (MONO._fixup_name_value_objects (this.var_info)); - for (var i = 0; i < res.length; i++) { - var res_val = res [i].value; - // we might not have a `.value`, like in case of getters which have a `.get` instead - if (res_val !== undefined && res_val.isValueType != undefined && res_val.isValueType) - res_val.objectId = `dotnet:valuetype:${objId}:${res [i].fieldOffset}`; - } - - this.var_info = []; + /** + * @param {number} idNum + * @param {boolean} expandValueTypes + * @returns {object} + */ + _get_object_properties: function(idNum, expandValueTypes) { + let { res_ok, res } = this.mono_wasm_get_object_properties_info (idNum, expandValueTypes); + if (!res_ok) + throw new Error (`Failed to get properties for ${idNum}`); + + res = MONO._filter_automatic_properties (res); + res = this._assign_vt_ids (res, v => ({ containerId: idNum, fieldOffset: v.fieldOffset })); + res = this._post_process_details (res); return res; }, - mono_wasm_get_array_values: function(objId) { - if (!this.mono_wasm_get_array_values_info) - this.mono_wasm_get_array_values_info = Module.cwrap ("mono_wasm_get_array_values", null, [ 'number' ]); - - this.var_info = []; - this.mono_wasm_get_array_values_info (objId); - - var res = MONO._fixup_name_value_objects (this.var_info); - for (var i = 0; i < res.length; i++) { - var prop_value = res [i].value; - if (prop_value.isValueType) { - res [i].value.objectId = `dotnet:array:${objId}:${i}`; - } else if (prop_value.objectId !== undefined && prop_value.objectId.startsWith("dotnet:pointer")) { - prop_value.objectId = this._get_updated_ptr_id (prop_value.objectId, { - varName: `[${i}]` - }); - } + /** + * @param {WasmId} id + * @param {number} [startIdx=0] + * @param {number} [count=-1] + * @param {boolean} [expandValueTypes=false] + * @returns {object[]} + */ + _get_array_values: function (id, startIdx = 0, count = -1, expandValueTypes = false) { + if (isNaN (id.o.arrayId) || isNaN (startIdx)) + throw new Error (`Invalid array id: ${id.idStr}`); + + let { res_ok, res } = this.mono_wasm_get_array_values_info (id.o.arrayId, startIdx, count, expandValueTypes); + if (!res_ok) + throw new Error (`Failed to get properties for array id ${id.idStr}`); + + res = this._assign_vt_ids (res, (_, i) => ({ arrayId: id.o.arrayId, arrayIdx: Number (startIdx) + i})); + + for (let i = 0; i < res.length; i ++) { + let value = res [i].value; + if (value.objectId !== undefined && value.objectId.startsWith("dotnet:pointer")) + this._new_or_add_id_props ({ objectId: value.objectId, props: { varName: `[${i}]` } }); } - - this.var_info = []; - - return res; - }, - - mono_wasm_get_array_value_expanded: function(objId, idx) { - if (!this.mono_wasm_get_array_value_expanded_info) - this.mono_wasm_get_array_value_expanded_info = Module.cwrap ("mono_wasm_get_array_value_expanded", null, [ 'number', 'number' ]); - - this.var_info = []; - this.mono_wasm_get_array_value_expanded_info (objId, idx); - - var res = MONO._fixup_name_value_objects (this.var_info); - // length should be exactly one! - if (res [0].value.isValueType != undefined && res [0].value.isValueType) - res [0].value.objectId = `dotnet:array:${objId}:${idx}`; - - this.var_info = []; - + res = this._post_process_details (res); return res; }, @@ -289,8 +337,13 @@ var MonoSupportLib = { return details; }, - _next_value_type_id: function () { - return ++this._next_value_type_id_var; + /** + * Gets the next id number to use for generating ids + * + * @returns {number} + */ + _next_id: function () { + return ++this._next_id_var; }, _extract_and_cache_value_types: function (var_list) { @@ -298,82 +351,46 @@ var MonoSupportLib = { return var_list; for (let i in var_list) { - var value = var_list [i].value; + let value = var_list [i].value; if (value === undefined) continue; if (value.objectId !== undefined && value.objectId.startsWith ("dotnet:pointer:")) { - var ptr_args = this._get_ptr_args (value.objectId); - if (ptr_args.varName === undefined) { - // It might have been already set in some cases, like arrays - // where the name would be `0`, but we want `[0]` for pointers, - // so the deref would look like `*[0]` - value.objectId = this._get_updated_ptr_id (value.objectId, { - varName: var_list [i].name - }); - } + let ptr_args = this._get_id_props (value.objectId); + if (ptr_args === undefined) + throw new Error (`Bug: Expected to find an entry for pointer id: ${value.objectId}`); + + // It might have been already set in some cases, like arrays + // where the name would be `0`, but we want `[0]` for pointers, + // so the deref would look like `*[0]` + ptr_args.varName = ptr_args.varName || var_list [i].name; } if (value.type != "object" || value.isValueType != true || value.expanded != true) // undefined would also give us false continue; // Generate objectId for expanded valuetypes - - var objectId = value.objectId; - if (objectId == undefined) - objectId = `dotnet:valuetype:${this._next_value_type_id ()}`; - value.objectId = objectId; + value.objectId = value.objectId || this._new_or_add_id_props ({ scheme: 'valuetype' }); this._extract_and_cache_value_types (value.members); - this._value_types_cache [objectId] = value.members; + const new_props = Object.assign ({ members: value.members }, value.__extra_vt_props); + + this._new_or_add_id_props ({ objectId: value.objectId, props: new_props }); delete value.members; + delete value.__extra_vt_props; } return var_list; }, - _get_details_for_value_type: function (objectId, fetchDetailsFn) { - if (objectId in this._value_types_cache) - return this._value_types_cache[objectId]; - - this._post_process_details (fetchDetailsFn()); - if (objectId in this._value_types_cache) - return this._value_types_cache[objectId]; - - // return error - throw new Error (`Could not get details for ${objectId}`); - }, - - _is_object_id_array: function (objectId) { - // Keep this in sync with `_get_array_details` - return (objectId.startsWith ('dotnet:array:') && objectId.split (':').length == 3); - }, - - _get_array_details: function (objectId, objectIdParts) { - // Keep this in sync with `_is_object_id_array` - switch (objectIdParts.length) { - case 3: - return this._post_process_details (this.mono_wasm_get_array_values(objectIdParts[2])); - - case 4: - var arrayObjectId = objectIdParts[2]; - var arrayIdx = objectIdParts[3]; - return this._get_details_for_value_type( - objectId, () => this.mono_wasm_get_array_value_expanded(arrayObjectId, arrayIdx)); - - default: - throw new Error (`object id format not supported : ${objectId}`); - } - }, - _get_cfo_res_details: function (objectId, args) { if (!(objectId in this._call_function_res_cache)) throw new Error(`Could not find any object with id ${objectId}`); - var real_obj = this._call_function_res_cache [objectId]; + const real_obj = this._call_function_res_cache [objectId]; - var descriptors = Object.getOwnPropertyDescriptors (real_obj); + const descriptors = Object.getOwnPropertyDescriptors (real_obj); if (args.accessorPropertiesOnly) { Object.keys (descriptors).forEach (k => { if (descriptors [k].get === undefined) @@ -381,10 +398,10 @@ var MonoSupportLib = { }); } - var res_details = []; + let res_details = []; Object.keys (descriptors).forEach (k => { - var new_obj; - var prop_desc = descriptors [k]; + let new_obj; + let prop_desc = descriptors [k]; if (typeof prop_desc.value == "object") { // convert `{value: { type='object', ... }}` // to `{ name: 'foo', value: { type='object', ... }} @@ -425,34 +442,77 @@ var MonoSupportLib = { return { __value_as_json_string__: JSON.stringify (res_details) }; }, - _get_ptr_args: function (objectId) { - var parts = this._split_object_id (objectId); - if (parts.length != 3) - throw new Error (`Bug: Unexpected objectId format for a pointer, expected 3 parts: ${objectId}`); - return JSON.parse (parts [2]); - }, + /** + * Generates a new id, and a corresponding entry for associated properties + * like `dotnet:pointer:{ a: 4 }` + * The third segment of that `{a:4}` is the idArgs parameter + * + * Only `scheme` or `objectId` can be set. + * if `scheme`, then a new id is generated, and it's properties set + * if `objectId`, then it's properties are updated + * + * @param {object} args + * @param {string} [args.scheme=undefined] scheme second part of `dotnet:pointer:..` + * @param {string} [args.objectId=undefined] objectId + * @param {object} [args.idArgs={}] The third segment of the objectId + * @param {object} [args.props={}] Properties for the generated id + * + * @returns {string} generated/updated id string + */ + _new_or_add_id_props: function ({ scheme = undefined, objectId = undefined, idArgs = {}, props = {} }) { + if (scheme === undefined && objectId === undefined) + throw new Error (`Either scheme or objectId must be given`); + + if (scheme !== undefined && objectId !== undefined) + throw new Error (`Both scheme, and objectId cannot be given`); + + if (objectId !== undefined && Object.entries (idArgs).length > 0) + throw new Error (`Both objectId, and idArgs cannot be given`); + + if (Object.entries (idArgs).length == 0) { + // We want to generate a new id, only if it doesn't have other + // attributes that it can use to uniquely identify. + // Eg, we don't do this for `dotnet:valuetype:{containerId:4, fieldOffset: 24}` + idArgs.num = this._next_id (); + } - _get_updated_ptr_id: function (objectId, new_args) { - var old_args = {}; - if (typeof (objectId) === 'string' && objectId.length) - old_args = this._get_ptr_args (objectId); + let idStr; + if (objectId !== undefined) { + idStr = objectId; + const old_props = this._id_table [idStr]; + if (old_props === undefined) + throw new Error (`ObjectId not found in the id table: ${idStr}`); - return `dotnet:pointer:${JSON.stringify ( Object.assign (old_args, new_args) )}`; + this._id_table [idStr] = Object.assign (old_props, props); + } else { + idStr = `dotnet:${scheme}:${JSON.stringify (idArgs)}`; + this._id_table [idStr] = props; + } + + return idStr; + }, + + /** + * @param {string} objectId + * @returns {object} + */ + _get_id_props: function (objectId) { + return this._id_table [objectId]; }, _get_deref_ptr_value: function (objectId) { - if (!this.mono_wasm_get_deref_ptr_value_info) - this.mono_wasm_get_deref_ptr_value_info = Module.cwrap("mono_wasm_get_deref_ptr_value", null, ['number', 'number']); + const ptr_args = this._get_id_props (objectId); + if (ptr_args === undefined) + throw new Error (`Unknown pointer id: ${objectId}`); - var ptr_args = this._get_ptr_args (objectId); if (ptr_args.ptr_addr == 0 || ptr_args.klass_addr == 0) throw new Error (`Both ptr_addr and klass_addr need to be non-zero, to dereference a pointer. objectId: ${objectId}`); - this.var_info = []; - var value_addr = new DataView (Module.HEAPU8.buffer).getUint32 (ptr_args.ptr_addr, /* littleEndian */ true); - this.mono_wasm_get_deref_ptr_value_info (value_addr, ptr_args.klass_addr); + const value_addr = new DataView (Module.HEAPU8.buffer).getUint32 (ptr_args.ptr_addr, /* littleEndian */ true); + let { res_ok, res } = this.mono_wasm_get_deref_ptr_value_info (value_addr, ptr_args.klass_addr); + if (!res_ok) + throw new Error (`Failed to dereference pointer ${objectId}`); - var res = MONO._fixup_name_value_objects(this.var_info); if (res.length > 0) { if (ptr_args.varName === undefined) throw new Error (`Bug: no varName found for the pointer. objectId: ${objectId}`); @@ -461,34 +521,25 @@ var MonoSupportLib = { } res = this._post_process_details (res); - this.var_info = []; return res; }, mono_wasm_get_details: function (objectId, args) { - var parts = objectId.split(":"); - if (parts[0] != "dotnet") - throw new Error ("Can't handle non-dotnet object ids. ObjectId: " + objectId); + let id = this._parse_object_id (objectId, true); - switch (parts[1]) { - case "object": - if (parts.length != 3) - throw new Error(`exception this time: Invalid object id format: ${objectId}`); + switch (id.scheme) { + case "object": { + if (isNaN (id.value)) + throw new Error (`Invalid objectId: ${objectId}. Expected a numeric id.`); - return this._post_process_details(this.mono_wasm_get_object_properties(parts[2], false)); + return this._get_object_properties(id.value, false); + } case "array": - return this._get_array_details(objectId, parts); + return this._get_array_values (id); case "valuetype": - if (parts.length != 3 && parts.length != 4) { - // dotnet:valuetype:vtid - // dotnet:valuetype:containerObjectId:vtId - throw new Error(`Invalid object id format: ${objectId}`); - } - - var containerObjectId = parts[2]; - return this._get_details_for_value_type(objectId, () => this.mono_wasm_get_object_properties(containerObjectId, true)); + return this._get_vt_properties(id); case "cfo_res": return this._get_cfo_res_details (objectId, args); @@ -503,7 +554,7 @@ var MonoSupportLib = { }, _cache_call_function_res: function (obj) { - var id = `dotnet:cfo_res:${this._next_call_function_res_id++}`; + const id = `dotnet:cfo_res:${this._next_call_function_res_id++}`; this._call_function_res_cache[id] = obj; return id; }, @@ -513,44 +564,69 @@ var MonoSupportLib = { delete this._cache_call_function_res[objectId]; }, - _invoke_getter_on_object: function (objectId, name) { - if (!this.mono_wasm_invoke_getter_on_object) - this.mono_wasm_invoke_getter_on_object = Module.cwrap ("mono_wasm_invoke_getter_on_object", 'void', [ 'number', 'string' ]); - - if (objectId < 0) { - // invalid id - return []; + /** + * @param {string} objectIdStr objectId + * @param {string} name property name + * @returns {object} return value + */ + _invoke_getter: function (objectIdStr, name) { + const id = this._parse_object_id (objectIdStr); + if (id === undefined) + throw new Error (`Invalid object id: ${objectIdStr}`); + + let getter_res; + if (id.scheme == 'object') { + if (isNaN (id.o) || id.o < 0) + throw new Error (`Invalid object id: ${objectIdStr}`); + + let { res_ok, res } = this.mono_wasm_invoke_getter_on_object_info (id.o, name); + if (!res_ok) + throw new Error (`Invoking getter on ${objectIdStr} failed`); + + getter_res = res; + } else if (id.scheme == 'valuetype') { + const id_props = this._get_id_props (objectIdStr); + if (id_props === undefined) + throw new Error (`Unknown valuetype id: ${objectIdStr}`); + + if (typeof id_props.value64 !== 'string' || isNaN (id_props.klass)) + throw new Error (`Bug: Cannot invoke getter on ${objectIdStr}, because of missing or invalid klass/value64 fields. idProps: ${JSON.stringify (id_props)}`); + + const dataPtr = Module._malloc (id_props.value64.length); + const dataHeap = new Uint8Array (Module.HEAPU8.buffer, dataPtr, id_props.value64.length); + dataHeap.set (new Uint8Array (this._base64_to_uint8 (id_props.value64))); + + let { res_ok, res } = this.mono_wasm_invoke_getter_on_value_info (dataHeap.byteOffset, id_props.klass, name); + Module._free (dataHeap.byteOffset); + + if (!res_ok) { + console.debug (`Invoking getter on valuetype ${objectIdStr}, with props: ${JSON.stringify (id_props)} failed`); + throw new Error (`Invoking getter on valuetype ${objectIdStr} failed`); + } + getter_res = res; + } else { + throw new Error (`Only object, and valuetypes supported for getters, id: ${objectIdStr}`); } - this.mono_wasm_invoke_getter_on_object (objectId, name); - var getter_res = MONO._post_process_details (MONO.var_info); - - MONO.var_info = []; - return getter_res [0]; + getter_res = MONO._post_process_details (getter_res); + return getter_res.length > 0 ? getter_res [0] : {}; }, _create_proxy_from_object_id: function (objectId) { - var details = this.mono_wasm_get_details(objectId); + const details = this.mono_wasm_get_details(objectId); - if (this._is_object_id_array (objectId)) + if (objectId.startsWith ('dotnet:array:')) return details.map (p => p.value); - var objIdParts = objectId.split (':'); - var objIdNum = -1; - if (objectId.startsWith ("dotnet:object:")) - objIdNum = objIdParts [2]; - - var proxy = {}; + let proxy = {}; Object.keys (details).forEach (p => { var prop = details [p]; if (prop.get !== undefined) { // TODO: `set` - // We don't add a `get` for non-object types right now, - // so, we shouldn't get here with objIdNum==-1 Object.defineProperty (proxy, prop.name, - { get () { return MONO._invoke_getter_on_object (objIdNum, prop.name); } } + { get () { return MONO._invoke_getter (objectId, prop.name); } } ); } else { proxy [prop.name] = prop.value; @@ -564,21 +640,27 @@ var MonoSupportLib = { if (request.arguments != undefined && !Array.isArray (request.arguments)) throw new Error (`"arguments" should be an array, but was ${request.arguments}`); - var objId = request.objectId; - var proxy; + const objId = request.objectId; + let proxy; - if (objId in this._call_function_res_cache) { - proxy = this._call_function_res_cache [objId]; - } else if (!objId.startsWith ('dotnet:cfo_res:')) { + if (objId.startsWith ('dotnet:cfo_res:')) { + if (objId in this._call_function_res_cache) + proxy = this._call_function_res_cache [objId]; + else + throw new Error (`Unknown object id ${objId}`); + } else { proxy = this._create_proxy_from_object_id (objId); } - var fn_args = request.arguments != undefined ? request.arguments.map(a => JSON.stringify(a.value)) : []; - var fn_eval_str = `var fn = ${request.functionDeclaration}; fn.call (proxy, ...[${fn_args}]);`; + const fn_args = request.arguments != undefined ? request.arguments.map(a => JSON.stringify(a.value)) : []; + const fn_eval_str = `var fn = ${request.functionDeclaration}; fn.call (proxy, ...[${fn_args}]);`; - var fn_res = eval (fn_eval_str); - if (fn_res == undefined) // should we just return undefined? - throw Error ('Function returned undefined result'); + const fn_res = eval (fn_eval_str); + if (fn_res === undefined) + return { type: "undefined" }; + + if (fn_res === null || (fn_res.subtype === 'null' && fn_res.value === undefined)) + return fn_res; // primitive type if (Object (fn_res) !== fn_res) @@ -591,7 +673,7 @@ var MonoSupportLib = { if (request.returnByValue) return {type: "object", value: fn_res}; - var fn_res_id = this._cache_call_function_res (fn_res); + const fn_res_id = this._cache_call_function_res (fn_res); if (Object.getPrototypeOf (fn_res) == Array.prototype) { return { type: "object", @@ -606,14 +688,14 @@ var MonoSupportLib = { }, _clear_per_step_state: function () { - this._next_value_type_id_var = 0; - this._value_types_cache = {}; + this._next_id_var = 0; + this._id_table = {}; }, mono_wasm_debugger_resume: function () { this._clear_per_step_state (); }, - + mono_wasm_start_single_stepping: function (kind) { console.log (">> mono_wasm_start_single_stepping " + kind); if (!this.mono_wasm_setup_single_step) @@ -639,6 +721,42 @@ var MonoSupportLib = { return this.mono_wasm_pause_on_exceptions (state_enum); }, + _register_c_fn: function (name, ...args) { + Object.defineProperty (this._c_fn_table, name + '_wrapper', { value: Module.cwrap (name, ...args) }); + }, + + /** + * Calls `Module.cwrap` for the function name, + * and creates a wrapper around it that returns + * `{ bool result, object var_info } + * + * @param {string} name C function name + * @param {string} ret_type + * @param {string[]} params + * + * @returns {void} + */ + _register_c_var_fn: function (name, ret_type, params) { + if (ret_type !== 'bool') + throw new Error (`Bug: Expected a C function signature that returns bool`); + + this._register_c_fn (name, ret_type, params); + Object.defineProperty (this, name + '_info', { + value: function (...args) { + MONO.var_info = []; + const res_ok = MONO._c_fn_table [name + '_wrapper'] (...args); + let res = MONO.var_info; + MONO.var_info = []; + if (res_ok) { + res = this._fixup_name_value_objects (res); + return { res_ok, res }; + } + + return { res_ok, res: undefined }; + } + }); + }, + mono_wasm_runtime_ready: function () { this.mono_wasm_runtime_is_ready = true; // DO NOT REMOVE - magic debugger init function @@ -649,6 +767,14 @@ var MonoSupportLib = { // FIXME: where should this go? this._next_call_function_res_id = 0; this._call_function_res_cache = {}; + + this._c_fn_table = {}; + this._register_c_var_fn ('mono_wasm_get_object_properties', 'bool', [ 'number', 'bool' ]); + this._register_c_var_fn ('mono_wasm_get_array_values', 'bool', [ 'number', 'number', 'number', 'bool' ]); + this._register_c_var_fn ('mono_wasm_invoke_getter_on_object', 'bool', [ 'number', 'string' ]); + this._register_c_var_fn ('mono_wasm_invoke_getter_on_value', 'bool', [ 'number', 'number', 'string' ]); + this._register_c_var_fn ('mono_wasm_get_local_vars', 'bool', [ 'number', 'number', 'number']); + this._register_c_var_fn ('mono_wasm_get_deref_ptr_value', 'bool', [ 'number', 'number']); }, mono_wasm_set_breakpoint: function (assembly, method_token, il_offset) { @@ -666,7 +792,7 @@ var MonoSupportLib = { }, // Set environment variable NAME to VALUE - // Should be called before mono_load_runtime_and_bcl () in most cases + // Should be called before mono_load_runtime_and_bcl () in most cases mono_wasm_setenv: function (name, value) { if (!this.wasm_setenv) this.wasm_setenv = Module.cwrap ('mono_wasm_setenv', null, ['string', 'string']); @@ -678,7 +804,7 @@ var MonoSupportLib = { this.wasm_parse_runtime_options = Module.cwrap ('mono_wasm_parse_runtime_options', null, ['number', 'number']); var argv = Module._malloc (options.length * 4); var wasm_strdup = Module.cwrap ('mono_wasm_strdup', 'number', ['string']); - aindex = 0; + let aindex = 0; for (var i = 0; i < options.length; ++i) { Module.setValue (argv + (aindex * 4), wasm_strdup (options [i]), "i32"); aindex += 1; @@ -1155,7 +1281,7 @@ var MonoSupportLib = { mono_wasm_add_null_var: function(className) { - fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className)); + let fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className)); if (!fixed_class_name) { // Eg, when a @className is passed from js itself, like // mono_wasm_add_null_var ("string") @@ -1179,12 +1305,13 @@ var MonoSupportLib = { value: { type: "string", value: var_value, + description: var_value } }); }, _mono_wasm_add_getter_var: function(className, invokable) { - fixed_class_name = MONO._mono_csharp_fixup_class_name (className); + const fixed_class_name = MONO._mono_csharp_fixup_class_name (className); if (invokable != 0) { var name; if (MONO.var_info.length > 0) @@ -1211,7 +1338,7 @@ var MonoSupportLib = { }, _mono_wasm_add_array_var: function(className, objectId, length) { - fixed_class_name = MONO._mono_csharp_fixup_class_name(className); + const fixed_class_name = MONO._mono_csharp_fixup_class_name(className); if (objectId == 0) { MONO.mono_wasm_add_null_var (fixed_class_name); return; @@ -1223,42 +1350,121 @@ var MonoSupportLib = { subtype: "array", className: fixed_class_name, description: `${fixed_class_name}(${length})`, - objectId: "dotnet:array:"+ objectId, + objectId: this._new_or_add_id_props ({ scheme: 'array', idArgs: { arrayId: objectId } }) } }); }, + // FIXME: improve + _base64_to_uint8: function (base64String) { + const byteCharacters = atob (base64String); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + + return new Uint8Array (byteNumbers); + }, + + _begin_value_type_var: function(className, args) { + if (args === undefined || (typeof args !== 'object')) { + console.debug (`_begin_value_type_var: Expected an args object`); + return; + } + + const fixed_class_name = MONO._mono_csharp_fixup_class_name(className); + const toString = args.toString; + const base64String = btoa (String.fromCharCode (...new Uint8Array (Module.HEAPU8.buffer, args.value_addr, args.value_size))); + const vt_obj = { + value: { + type : "object", + className : fixed_class_name, + description : (toString == 0 ? fixed_class_name: Module.UTF8ToString (toString)), + expanded : true, + isValueType : true, + __extra_vt_props: { klass: args.klass, value64: base64String }, + members : [] + } + }; + if (MONO._vt_stack.length == 0) + MONO._old_var_info = MONO.var_info; + + MONO.var_info = vt_obj.value.members; + MONO._vt_stack.push (vt_obj); + }, + + _end_value_type_var: function() { + let top_vt_obj_popped = MONO._vt_stack.pop (); + top_vt_obj_popped.value.members = MONO._filter_automatic_properties ( + MONO._fixup_name_value_objects (top_vt_obj_popped.value.members)); + + if (MONO._vt_stack.length == 0) { + MONO.var_info = MONO._old_var_info; + MONO.var_info.push(top_vt_obj_popped); + } else { + var top_obj = MONO._vt_stack [MONO._vt_stack.length - 1]; + top_obj.value.members.push (top_vt_obj_popped); + MONO.var_info = top_obj.value.members; + } + }, + + _add_valuetype_unexpanded_var: function(className, args) { + if (args === undefined || (typeof args !== 'object')) { + console.debug (`_add_valuetype_unexpanded_var: Expected an args object`); + return; + } + + const fixed_class_name = MONO._mono_csharp_fixup_class_name (className); + const toString = args.toString; + + MONO.var_info.push ({ + value: { + type: "object", + className: fixed_class_name, + description: (toString == 0 ? fixed_class_name : Module.UTF8ToString (toString)), + isValueType: true + } + }); + }, + + mono_wasm_add_typed_value: function (type, str_value, value) { - var type_str = type; + let type_str = type; if (typeof type != 'string') type_str = Module.UTF8ToString (type); - if (typeof str_value != 'string') str_value = Module.UTF8ToString (str_value); switch (type_str) { - case "bool": + case "bool": { + const v = value != 0; MONO.var_info.push ({ value: { type: "boolean", - value: value != 0 + value: v, + description: v.toString () } }); break; + } - case "char": + case "char": { + const v = `${value} '${String.fromCharCode (value)}'`; MONO.var_info.push ({ value: { type: "symbol", - value: `${value} '${String.fromCharCode (value)}'` + value: v, + description: v } }); break; + } case "number": MONO.var_info.push ({ value: { type: "number", - value: value + value: value, + description: '' + value } }); break; @@ -1275,8 +1481,20 @@ var MonoSupportLib = { MONO._mono_wasm_add_array_var (str_value, value.objectId, value.length); break; + case "begin_vt": + MONO._begin_value_type_var (str_value, value); + break; + + case "end_vt": + MONO._end_value_type_var (); + break; + + case "unexpanded_vt": + MONO._add_valuetype_unexpanded_var (str_value, value); + break; + case "pointer": { - var fixed_value_str = MONO._mono_csharp_fixup_class_name (str_value); + const fixed_value_str = MONO._mono_csharp_fixup_class_name (str_value); if (value.klass_addr == 0 || value.ptr_addr == 0 || fixed_value_str.startsWith ('(void*')) { // null or void*, which we can't deref MONO.var_info.push({ @@ -1292,7 +1510,7 @@ var MonoSupportLib = { type: "object", className: fixed_value_str, description: fixed_value_str, - objectId: this._get_updated_ptr_id ('', value) + objectId: this._new_or_add_id_props ({ scheme: 'pointer', props: value }) } }); } @@ -1300,7 +1518,7 @@ var MonoSupportLib = { break; default: { - var msg = `'${str_value}' ${value}`; + const msg = `'${str_value}' ${value}`; MONO.var_info.push ({ value: { @@ -1390,55 +1608,6 @@ var MonoSupportLib = { MONO._async_method_objectId = objectId; }, - mono_wasm_begin_value_type_var: function(className, toString) { - fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className)); - var vt_obj = { - value: { - type: "object", - className: fixed_class_name, - description: (toString == 0 ? fixed_class_name : Module.UTF8ToString (toString)), - // objectId will be generated by MonoProxy - expanded: true, - isValueType: true, - members: [] - } - }; - if (MONO._vt_stack.length == 0) - MONO._old_var_info = MONO.var_info; - - MONO.var_info = vt_obj.value.members; - MONO._vt_stack.push (vt_obj); - }, - - mono_wasm_end_value_type_var: function() { - var top_vt_obj_popped = MONO._vt_stack.pop (); - top_vt_obj_popped.value.members = MONO._filter_automatic_properties ( - MONO._fixup_name_value_objects (top_vt_obj_popped.value.members)); - - if (MONO._vt_stack.length == 0) { - MONO.var_info = MONO._old_var_info; - MONO.var_info.push(top_vt_obj_popped); - } else { - var top_obj = MONO._vt_stack [MONO._vt_stack.length - 1]; - top_obj.value.members.push (top_vt_obj_popped); - MONO.var_info = top_obj.value.members; - } - }, - - mono_wasm_add_value_type_unexpanded_var: function (className, toString) { - fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className)); - MONO.var_info.push({ - value: { - type: "object", - className: fixed_class_name, - description: (toString == 0 ? fixed_class_name : Module.UTF8ToString (toString)), - // objectId added when enumerating object's properties - expanded: false, - isValueType: true - } - }); - }, - mono_wasm_add_enum_var: function(className, members, value) { // FIXME: flags // @@ -1446,13 +1615,13 @@ var MonoSupportLib = { // group0: Monday:0 // group1: Monday // group2: 0 - var re = new RegExp (`[,]?([^,:]+):(${value}(?=,)|${value}$)`, 'g') - var members_str = Module.UTF8ToString (members); + const re = new RegExp (`[,]?([^,:]+):(${value}(?=,)|${value}$)`, 'g') + const members_str = Module.UTF8ToString (members); - var match = re.exec(members_str); - var member_name = match == null ? ('' + value) : match [1]; + const match = re.exec(members_str); + const member_name = match == null ? ('' + value) : match [1]; - fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className)); + const fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className)); MONO.var_info.push({ value: { type: "object", @@ -1475,7 +1644,7 @@ var MonoSupportLib = { return; } - fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className)); + const fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className)); MONO.var_info.push({ value: { type: "object", @@ -1512,11 +1681,11 @@ var MonoSupportLib = { return `${ret_sig} ${method_name} (${args_sig})`; } - var tgt_sig; + let tgt_sig; if (targetName != 0) tgt_sig = args_to_sig (Module.UTF8ToString (targetName)); - var type_name = MONO._mono_csharp_fixup_class_name (Module.UTF8ToString (className)); + const type_name = MONO._mono_csharp_fixup_class_name (Module.UTF8ToString (className)); if (objectId == -1) { // Target property @@ -1586,6 +1755,7 @@ var MonoSupportLib = { mono_wasm_fire_bp: function () { console.log ("mono_wasm_fire_bp"); + // eslint-disable-next-line no-debugger debugger; }, -- 2.7.4