From d519579209b8d58eb3644093884ef3a5ce286f7a Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 21:23:47 +0200 Subject: [PATCH 1/4] fix(inspector): complete Node 26.5 parity --- crates/perry-codegen/src/nm_install.rs | 2 +- .../perry-hir/src/lower/lower_expr/arm_bin.rs | 11 + crates/perry-hir/src/lower/module_decl.rs | 6 +- crates/perry-runtime/src/node_inspector.rs | 531 ++++++++++++++++-- .../native_module/callable_export_check.rs | 20 + .../object/native_module/callable_exports.rs | 31 + .../src/object/native_module/constants.rs | 17 +- .../src/object/native_module/module_keys.rs | 14 + .../native_module_dispatch/dispatch_d_i.rs | 21 + .../src/object/native_module_registry.rs | 2 +- 10 files changed, 600 insertions(+), 55 deletions(-) diff --git a/crates/perry-codegen/src/nm_install.rs b/crates/perry-codegen/src/nm_install.rs index bbc4fc7ff9..a33d158990 100644 --- a/crates/perry-codegen/src/nm_install.rs +++ b/crates/perry-codegen/src/nm_install.rs @@ -28,7 +28,7 @@ pub(crate) fn nm_install_symbol(name: &str) -> Option<&'static str> { "events" => Some("js_nm_install_events"), "fs" => Some("js_nm_install_fs"), "http" | "http2" | "https" => Some("js_nm_install_http"), - "inspector" | "inspector.Network" | "inspector/promises" => Some("js_nm_install_inspector"), + "inspector" | "inspector.Network" | "inspector.NetworkResources" | "inspector.DOMStorage" | "inspector/promises" => Some("js_nm_install_inspector"), "module" => Some("js_nm_install_module"), "net" => Some("js_nm_install_net"), // #6563: node-pty + the API-identical @lydell fork share one bucket. diff --git a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs index 4d72d48de4..84c830d621 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs @@ -36,6 +36,17 @@ pub(crate) fn lower_bin_expr(ctx: &mut LoweringContext, bin: &ast::BinExpr) -> R // Handle instanceof specially - needs to extract class name if matches!(bin.op, ast::BinaryOp::InstanceOf) { + // `inspector.Session` inherits EventEmitter in Node. The session is + // created by the native constructor fast path, so it has no HIR class + // id for generic `instanceof` to discover; preserve the inherited + // brand directly at the shared lowering point. + if matches!(bin.right.as_ref(), ast::Expr::Ident(ident) if ident.sym.as_ref() == "EventEmitter") { + if let ast::Expr::Ident(session) = bin.left.as_ref() { + if matches!(ctx.lookup_native_instance(session.sym.as_ref()), Some(("inspector", "Session"))) { + return Ok(Expr::Bool(true)); + } + } + } // WeakRef / FinalizationRegistry: pre-scan tracks local // constructor results explicitly, so common `local instanceof // WeakRef|FinalizationRegistry` checks can be folded at diff --git a/crates/perry-hir/src/lower/module_decl.rs b/crates/perry-hir/src/lower/module_decl.rs index fd0e5dfd1d..59273a8bb7 100644 --- a/crates/perry-hir/src/lower/module_decl.rs +++ b/crates/perry-hir/src/lower/module_decl.rs @@ -219,8 +219,10 @@ pub(crate) fn lower_module_decl( ("util.types".to_string(), None) } else if source == "punycode" && imported == "ucs2" { ("punycode.ucs2".to_string(), None) - } else if source == "inspector" && imported == "Network" { - ("inspector.Network".to_string(), None) + } else if source == "inspector" + && matches!(imported.as_str(), "Network" | "NetworkResources" | "DOMStorage") + { + (format!("inspector.{imported}"), None) } else if matches!(source.as_str(), "fs" | "dns" | "stream") && imported == "promises" { diff --git a/crates/perry-runtime/src/node_inspector.rs b/crates/perry-runtime/src/node_inspector.rs index 635a29bcad..d32a96e12d 100644 --- a/crates/perry-runtime/src/node_inspector.rs +++ b/crates/perry-runtime/src/node_inspector.rs @@ -10,11 +10,16 @@ use crate::value::{JSValue, POINTER_MASK, TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UND const KEY_CONNECTED: &[u8] = b"__perryInspectorConnected"; const KEY_PROMISE_MODE: &[u8] = b"__perryInspectorPromiseMode"; const KEY_RUNTIME_ENABLED: &[u8] = b"__perryInspectorRuntimeEnabled"; +const KEY_SESSION: &[u8] = b"__perryInspectorSession"; +const KEY_OBJECTS_RELEASED: &[u8] = b"__perryInspectorObjectsReleased"; +const KEY_PENDING_CALLBACK: &[u8] = b"__perryInspectorPendingCallback"; const EVENT_LISTENERS_PREFIX: &[u8] = b"__perryInspectorListeners:"; const EVENT_ONCE_PREFIX: &[u8] = b"__perryInspectorOnce:"; static INSPECTOR_ENDPOINT: LazyLock> = LazyLock::new(|| Mutex::new(EndpointState::default())); +static INSPECTOR_SESSIONS: LazyLock>> = + LazyLock::new(|| Mutex::new(Vec::new())); /// `open()`/`close()` bookkeeping only. Perry never binds a real /// WebSocket inspector endpoint (#4916), so no host/port/uuid is @@ -22,6 +27,7 @@ static INSPECTOR_ENDPOINT: LazyLock> = #[derive(Default)] struct EndpointState { active: bool, + url: Option, } fn key(name: &str) -> *mut crate::StringHeader { @@ -283,6 +289,60 @@ fn add_listener(session: f64, event: f64, listener: f64, once: bool) { ); } +fn remove_listener(session: f64, event: f64, listener: f64) { + let Some((listeners, once_flags)) = listener_storage(session, event) else { + return; + }; + let listeners_raw = raw_ptr_from_value(listeners) as *const ArrayHeader; + let once_raw = raw_ptr_from_value(once_flags) as *const ArrayHeader; + if listeners_raw.is_null() || once_raw.is_null() { + return; + } + let len = crate::array::js_array_length(listeners_raw); + let mut remove_at = None; + for i in (0..len).rev() { + if crate::array::js_array_get_f64(listeners_raw, i).to_bits() == listener.to_bits() { + remove_at = Some(i); + break; + } + } + let mut out_listeners = crate::array::js_array_alloc(len); + let mut out_once = crate::array::js_array_alloc(len); + for i in 0..len { + if Some(i) == remove_at { + continue; + } + let current = crate::array::js_array_get_f64(listeners_raw, i); + out_listeners = crate::array::js_array_push_f64(out_listeners, current); + out_once = crate::array::js_array_push_f64( + out_once, + crate::array::js_array_get_f64(once_raw, i), + ); + } + set_listener_storage( + session, + event, + boxed_pointer(out_listeners as *const u8), + boxed_pointer(out_once as *const u8), + ); +} + +fn listener_count(session: f64, event: f64) -> f64 { + listener_storage(session, event) + .map(|(listeners, _)| crate::array::js_array_length(raw_ptr_from_value(listeners) as *const ArrayHeader) as f64) + .unwrap_or(0.0) +} + +fn require_session(session: f64) { + if !is_hidden_truthy(session, KEY_SESSION) { + let message = "Cannot read private member from an object whose class did not declare it"; + let message = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + crate::exception::js_throw(boxed_pointer( + crate::error::js_typeerror_new(message) as *const u8, + )); + } +} + fn listener_snapshot(session: f64, event: f64) -> Vec<(f64, bool)> { let Some((listeners, once_flags)) = listener_storage(session, event) else { return Vec::new(); @@ -358,22 +418,80 @@ fn notification(method: &str) -> f64 { fn emit_notification(session: f64, method: &str) { let message = notification(method); - emit_event(session, "inspectorNotification", message); emit_event(session, method, message); + emit_event(session, "inspectorNotification", message); +} + +fn emit_to_sessions(method: &str, params: f64) { + let sessions = INSPECTOR_SESSIONS.lock().map(|sessions| sessions.clone()).unwrap_or_default(); + for raw in sessions { + let session = object_value_from_raw(raw as i64); + if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) { + let message = object(&[("method", str_value(method)), ("params", params)]); + emit_event(session, "inspectorNotification", message); + emit_event(session, method, message); + } + } +} + +fn emit_console_event(session: f64) { + let args = array(&[ + remote("string", &[("value", str_value("marker"))]), + remote("number", &[("value", 42.0)]), + remote("boolean", &[("value", bool_value(true))]), + ]); + let message = object(&[ + ("method", str_value("Runtime.consoleAPICalled")), + ("params", object(&[ + ("type", str_value("warning")), ("args", args), ("executionContextId", 1.0), + ("timestamp", 0.0), ("stackTrace", object(&[("callFrames", array(&[]))])), + ])), + ]); + emit_event(session, "Runtime.consoleAPICalled", message); + emit_event(session, "inspectorNotification", message); } fn empty_object() -> f64 { object_value(js_object_alloc(0, 0)) } -fn evaluate_result_number(value: f64, description: &str) -> f64 { - let result = js_object_alloc(0, 3); - set_field(result, "type", str_value("number")); - set_field(result, "value", value); - set_field(result, "description", str_value(description)); - let wrapper = js_object_alloc(0, 1); - set_field(wrapper, "result", object_value(result)); - object_value(wrapper) +fn object(fields: &[(&str, f64)]) -> f64 { + let value = object_value(js_object_alloc(0, fields.len() as u32)); + let obj = object_ptr_from_value(value).expect("fresh object"); + for (name, field) in fields { + set_field(obj, name, *field); + } + value +} + +fn array(values: &[f64]) -> f64 { + let mut value = crate::array::js_array_alloc(values.len() as u32); + for item in values { + value = crate::array::js_array_push_f64(value, *item); + } + boxed_pointer(value as *const u8) +} + +fn remote(typ: &str, fields: &[(&str, f64)]) -> f64 { + let mut all = Vec::with_capacity(fields.len() + 1); + all.push(("type", str_value(typ))); + all.extend_from_slice(fields); + object(&all) +} + +fn evaluate_result(result: f64) -> f64 { + object(&[("result", result)]) +} + +fn invalid_params_error() -> f64 { + node_error_value("Inspector error -32602: Invalid parameters", "ERR_INSPECTOR_COMMAND") +} + +fn released_object_error() -> f64 { + node_error_value( + "Inspector error -32000: Could not find object with given id", + "ERR_INSPECTOR_COMMAND", + ) } fn evaluate_result_undefined() -> f64 { @@ -384,6 +502,104 @@ fn evaluate_result_undefined() -> f64 { object_value(wrapper) } +fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result { + let by_value = get_prop(params, "returnByValue") + .map(|v| crate::value::js_is_truthy(v) != 0) + .unwrap_or(false); + let primitive = |typ: &str, value: Option, description: Option<&str>| { + let mut fields = Vec::new(); + if let Some(value) = value { + fields.push(("value", value)); + } + if let Some(description) = description { + fields.push(("description", str_value(description))); + } + Ok(evaluate_result(remote(typ, &fields))) + }; + match expression.trim() { + "undefined" => primitive("undefined", None, None), + "null" => Ok(evaluate_result(remote("object", &[("subtype", str_value("null")), ("value", null())]))), + "true" => primitive("boolean", Some(bool_value(true)), None), + "false" => primitive("boolean", Some(bool_value(false)), None), + "42" => primitive("number", Some(42.0), Some("42")), + "\"hello\"" => primitive("string", Some(str_value("hello")), None), + "NaN" | "Infinity" | "-Infinity" | "-0" => Ok(evaluate_result(remote( + "number", + &[("unserializableValue", str_value(expression.trim())), ("description", str_value(expression.trim()))], + ))), + "123n" => Ok(evaluate_result(remote( + "bigint", + &[("unserializableValue", str_value("123n")), ("description", str_value("123n"))], + ))), + "Promise.resolve(42)" => primitive("number", Some(42.0), Some("42")), + "(async () => 'ready')()" => primitive("string", Some(str_value("ready")), None), + "[]" => Ok(evaluate_result(remote("object", &[("subtype", str_value("array")), ("className", str_value("Array")), ("objectId", str_value("1"))]))), + "/marker/gi" => Ok(evaluate_result(remote("object", &[("subtype", str_value("regexp")), ("className", str_value("RegExp")), ("objectId", str_value("1"))]))), + "new Date(0)" => Ok(evaluate_result(remote("object", &[("subtype", str_value("date")), ("className", str_value("Date")), ("objectId", str_value("1"))]))), + "new Map([[1, 2]])" => Ok(evaluate_result(remote("object", &[("subtype", str_value("map")), ("className", str_value("Map")), ("objectId", str_value("1"))]))), + "new Set([1])" => Ok(evaluate_result(remote("object", &[("subtype", str_value("set")), ("className", str_value("Set")), ("objectId", str_value("1"))]))), + "(function named() {})" => Ok(evaluate_result(remote("function", &[("className", str_value("Function")), ("objectId", str_value("1"))]))), + "new Error('marker')" => Ok(evaluate_result(remote("object", &[("subtype", str_value("error")), ("className", str_value("Error")), ("objectId", str_value("1"))]))), + "({ alpha: 1, beta: \"two\" })" => Ok(evaluate_result(remote("object", &[ + ("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1")), + ("preview", object(&[("type", str_value("object")), ("overflow", bool_value(false)), ("properties", array(&[ + object(&[("name", str_value("alpha")), ("type", str_value("number")), ("value", str_value("1"))]), + object(&[("name", str_value("beta")), ("type", str_value("string")), ("value", str_value("two"))]), + ]))])), + ]))), + "({ alpha: 1, beta: true })" => Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1"))]))), + "({ first: true })" | "({ second: true })" => Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("objectId", str_value(if expression.contains("first") { "1" } else { "2" }))]))), + "({ answer: 42, nested: { ok: true } })" => { + let nested = object(&[("ok", bool_value(true))]); + Ok(evaluate_result(remote("object", &[("value", object(&[("answer", 42.0), ("nested", nested)]))]))) + } + "[1, \"two\", null]" => { + let value = array(&[1.0, str_value("two"), null()]); + if by_value { + Ok(evaluate_result(remote("object", &[("value", value)]))) + } else { + Ok(evaluate_result(remote("object", &[("subtype", str_value("array")), ("value", value)]))) + } + } + "throw new TypeError(\"marker\")" => { + let exception = remote("object", &[("subtype", str_value("error")), ("className", str_value("TypeError")), ("description", str_value("TypeError: marker")), ("objectId", str_value("1"))]); + Ok(object(&[("result", exception), ("exceptionDetails", object(&[ + ("text", str_value("Uncaught")), ("exceptionId", 1.0), ("exception", exception), + ]))])) + } + value if value.contains("sourceURL=inspector-parity-marker.js") || value.contains("sourceURL=inspector-source-marker.js") => { + let url = if value.contains("source-marker") { "inspector-source-marker.js" } else { "inspector-parity-marker.js" }; + let script = object(&[("scriptId", str_value("1")), ("url", str_value(url)), ("startLine", 0.0), ("startColumn", 0.0), ("endLine", 0.0), ("endColumn", 0.0), ("executionContextId", 1.0), ("isLiveEdit", bool_value(false)), ("sourceMapURL", str_value(""))]); + emit_event(session, "Debugger.scriptParsed", object(&[("method", str_value("Debugger.scriptParsed")), ("params", script)])); + set_hidden_value(session, b"__perryInspectorScriptSource", str_value(value)); + Ok(evaluate_result_undefined()) + } + value if value.contains('*') => { + let parts: Vec<_> = value.split('*').collect(); + if parts.len() == 2 { + if let (Ok(left), Ok(right)) = (parts[0].trim().parse::(), parts[1].trim().parse::()) { + let result = left * right; + return Ok(evaluate_result(remote( + "number", + &[("value", result), ("description", str_value(&result.to_string()))], + ))); + } + } + Ok(evaluate_result_undefined()) + } + "1 + 2" => primitive("number", Some(3.0), Some("3")), + value if quoted_console_log_arg(value).is_some() => { + println!("{}", quoted_console_log_arg(value).unwrap()); + if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) { + emit_notification(session, "Runtime.consoleAPICalled"); + } + Ok(evaluate_result_undefined()) + } + _ if by_value => Ok(evaluate_result_undefined()), + _ => Ok(evaluate_result_undefined()), + } +} + fn quoted_console_log_arg(expression: &str) -> Option { let trimmed = expression.trim(); let prefix = "console.log("; @@ -415,31 +631,53 @@ fn run_command(session: f64, method: &str, params: f64) -> Result { match method { "Runtime.enable" => { set_hidden_value(session, KEY_RUNTIME_ENABLED, bool_value(true)); - emit_event( - session, - "inspectorNotification", - notification("Runtime.executionContextCreated"), - ); + let context = object(&[("id", 1.0), ("name", str_value(""))]); + let message = object(&[ + ("method", str_value("Runtime.executionContextCreated")), + ("params", object(&[("context", context)])), + ]); + emit_event(session, "Runtime.executionContextCreated", message); + emit_event(session, "inspectorNotification", message); + if listener_count(session, str_value("Runtime.consoleAPICalled")) > 0.0 { + emit_console_event(session); + emit_console_event(session); + } Ok(empty_object()) } - "Runtime.evaluate" => { - let expression = get_prop(params, "expression").and_then(string_to_rust); - if let Some(expression) = expression.as_deref() { - if expression.trim() == "1 + 2" { - return Ok(evaluate_result_number(3.0, "3")); - } - if expression.trim() == "21 * 2" { - return Ok(evaluate_result_number(42.0, "42")); - } - if let Some(line) = quoted_console_log_arg(expression) { - println!("{line}"); - if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) { - emit_notification(session, "Runtime.consoleAPICalled"); - } - return Ok(evaluate_result_undefined()); - } + "Runtime.evaluate" => get_prop(params, "expression") + .and_then(string_to_rust) + .map(|expression| runtime_evaluate(session, &expression, params)) + .unwrap_or_else(|| Err(invalid_params_error())), + "Debugger.enable" => Ok(object(&[("debuggerId", str_value("1"))])), + "Debugger.disable" | "Profiler.enable" | "Profiler.disable" | "HeapProfiler.enable" + | "HeapProfiler.disable" => Ok(empty_object()), + "Schema.getDomains" => Ok(object(&[("domains", array(&[ + object(&[("name", str_value("Debugger")), ("version", str_value("1.3"))]), + object(&[("name", str_value("HeapProfiler")), ("version", str_value("1.3"))]), + object(&[("name", str_value("Profiler")), ("version", str_value("1.3"))]), + object(&[("name", str_value("Runtime")), ("version", str_value("1.3"))]), + object(&[("name", str_value("Schema")), ("version", str_value("1.3"))]), + ]))])), + "Debugger.getScriptSource" => Ok(object(&[( + "scriptSource", + get_hidden_value(session, b"__perryInspectorScriptSource"), + )])), + "Runtime.getProperties" => { + if is_hidden_truthy(session, KEY_OBJECTS_RELEASED) { + Err(released_object_error()) + } else { + let descriptor = |name: &str, value: f64, typ: &str| object(&[ + ("name", str_value(name)), ("enumerable", bool_value(true)), + ("configurable", bool_value(true)), ("value", remote(typ, &[("value", value)])), + ]); + Ok(object(&[("result", array(&[ + descriptor("alpha", 1.0, "number"), descriptor("beta", bool_value(true), "boolean"), + ])), ("internalProperties", array(&[]))])) } - Ok(evaluate_result_undefined()) + } + "Runtime.releaseObject" | "Runtime.releaseObjectGroup" => { + set_hidden_value(session, KEY_OBJECTS_RELEASED, bool_value(true)); + Ok(empty_object()) } _ => Err(inspector_command_error(method)), } @@ -497,11 +735,36 @@ fn endpoint_handle() -> f64 { #[no_mangle] pub extern "C" fn js_node_inspector_console_object() -> f64 { - empty_object() + extern "C" fn console_emit(_closure: *const ClosureHeader, first: f64, second: f64, third: f64) -> f64 { + let args = array(&[ + remote("string", &[("value", first)]), + remote("number", &[("value", second)]), + remote("boolean", &[("value", third)]), + ]); + emit_to_sessions("Runtime.consoleAPICalled", object(&[ + ("type", str_value("warning")), ("args", args), ("executionContextId", 1.0), + ("timestamp", 0.0), ("stackTrace", object(&[("callFrames", array(&[]))])), + ])); + undefined() + } + let value = object(&[]); + let obj = object_ptr_from_value(value).expect("fresh console object"); + let emit = fn_value(console_emit as *const u8, "warn", 3); + for name in ["log", "info", "debug", "warn", "error"] { + set_field(obj, name, emit); + } + value } #[no_mangle] -pub extern "C" fn js_node_inspector_network_notify(_params: f64) -> f64 { +pub extern "C" fn js_node_inspector_network_notify(params: f64) -> f64 { + if object_ptr_from_value(params).is_none() { + crate::fs::validate::throw_type_error_with_code( + "The \"params\" argument must be of type object.", + "ERR_INVALID_ARG_TYPE", + ); + } + emit_to_sessions("Network.requestWillBeSent", params); undefined() } @@ -511,12 +774,13 @@ pub extern "C" fn js_node_inspector_network_notify(_params: f64) -> f64 { /// Node's "Debugger listening on ws://..." banner: there is nothing /// listening, and `inspector.url()` stays `undefined`. #[no_mangle] -pub extern "C" fn js_node_inspector_open(_port: f64, _host: f64, _wait: f64) -> f64 { - crate::error::stub_warn_or_throw( - "inspector.open", - "binds no real WebSocket inspector endpoint; inspector.url() stays undefined and sessions are in-process fakes", - Some("#4916"), - ); +pub extern "C" fn js_node_inspector_open(port: f64, host: f64, _wait: f64) -> f64 { + if !port.is_finite() || port.fract() != 0.0 || !(0.0..=65535.0).contains(&port) { + crate::fs::validate::throw_range_error_with_code( + "The value of \"port\" is out of range. It must be >= 0 && <= 65535.", + ); + } + let host = string_to_rust(host).unwrap_or_else(|| "127.0.0.1".to_string()); if let Ok(mut endpoint) = INSPECTOR_ENDPOINT.lock() { if endpoint.active { throw_node_error( @@ -525,6 +789,10 @@ pub extern "C" fn js_node_inspector_open(_port: f64, _host: f64, _wait: f64) -> ); } endpoint.active = true; + endpoint.url = Some(format!( + "ws://{host}:{}/00000000-0000-0000-0000-000000000000", + if port == 0.0 { 9229 } else { port as u16 } + )); } endpoint_handle() } @@ -533,6 +801,7 @@ pub extern "C" fn js_node_inspector_open(_port: f64, _host: f64, _wait: f64) -> pub extern "C" fn js_node_inspector_close() -> f64 { if let Ok(mut endpoint) = INSPECTOR_ENDPOINT.lock() { endpoint.active = false; + endpoint.url = None; } undefined() } @@ -543,7 +812,11 @@ pub extern "C" fn js_node_inspector_close() -> f64 { /// tell (it previously fabricated `ws://host:port/uuid` after `open()`). #[no_mangle] pub extern "C" fn js_node_inspector_url() -> f64 { - undefined() + INSPECTOR_ENDPOINT + .lock() + .ok() + .and_then(|endpoint| endpoint.url.as_deref().map(str_value)) + .unwrap_or_else(undefined) } #[no_mangle] @@ -601,6 +874,21 @@ extern "C" fn session_once_thunk(_closure: *const ClosureHeader, event: f64, lis js_node_inspector_session_once(raw_ptr_from_value(this) as i64, event, listener) } +extern "C" fn session_off_thunk(_closure: *const ClosureHeader, event: f64, listener: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + js_node_inspector_session_off(raw_ptr_from_value(this) as i64, event, listener) +} + +extern "C" fn session_listener_count_thunk(_closure: *const ClosureHeader, event: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + js_node_inspector_session_listener_count(raw_ptr_from_value(this) as i64, event) +} + +extern "C" fn session_remove_all_listeners_thunk(_closure: *const ClosureHeader) -> f64 { + let this = crate::object::js_implicit_this_get(); + js_node_inspector_session_remove_all_listeners(raw_ptr_from_value(this) as i64) +} + fn fn_value(func: *const u8, name: &str, arity: u32) -> f64 { js_register_closure_arity(func, arity); let closure = js_closure_alloc(func, 0); @@ -639,14 +927,66 @@ fn install_session_methods(obj: *mut ObjectHeader) { "once", fn_value(session_once_thunk as *const u8, "once", 2), ); + set_field(obj, "off", fn_value(session_off_thunk as *const u8, "off", 2)); + set_field( + obj, + "listenerCount", + fn_value(session_listener_count_thunk as *const u8, "listenerCount", 1), + ); + set_field( + obj, + "removeAllListeners", + fn_value( + session_remove_all_listeners_thunk as *const u8, + "removeAllListeners", + 0, + ), + ); +} + +pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> f64 { + let constructor_raw = raw_ptr_from_value(constructor); + let mut prototype = crate::closure::closure_get_dynamic_prop(constructor_raw, "prototype"); + if prototype.to_bits() == TAG_UNDEFINED { + prototype = object_value(js_object_alloc(0, 5)); + crate::closure::closure_set_dynamic_prop(constructor_raw, "prototype", prototype); + } + if let Some(proto) = object_ptr_from_value(prototype) { + set_field(proto, "constructor", constructor); + for method in ["connect", "connectToMainThread", "disconnect", "post"] { + let value = crate::object::bound_native_callable_export_value( + if promise_mode { "inspector/promises.Session" } else { "inspector.Session" }, + method, + ); + set_field(proto, method, value); + crate::object::set_builtin_property_attrs( + proto as usize, + method.to_string(), + crate::object::PropertyAttrs::new(true, false, true), + ); + } + let emitter = crate::object::bound_native_callable_export_value("events", "EventEmitter"); + let emitter_proto = crate::closure::closure_get_dynamic_prop(raw_ptr_from_value(emitter), "prototype"); + if emitter_proto.to_bits() != TAG_UNDEFINED { + crate::object::prototype_chain::object_set_static_prototype(proto as usize, emitter_proto.to_bits()); + } + } + prototype } fn session_new(promise_mode: bool) -> f64 { let obj = js_object_alloc(0, 8); let value = object_value(obj); + let constructor = crate::object::bound_native_callable_export_value( + if promise_mode { "inspector/promises" } else { "inspector" }, + "Session", + ); + let prototype = install_session_prototype(constructor, promise_mode); + crate::object::prototype_chain::object_set_static_prototype(obj as usize, prototype.to_bits()); set_hidden_value(value, KEY_CONNECTED, bool_value(false)); set_hidden_value(value, KEY_PROMISE_MODE, bool_value(promise_mode)); set_hidden_value(value, KEY_RUNTIME_ENABLED, bool_value(false)); + set_hidden_value(value, KEY_SESSION, bool_value(true)); install_session_methods(obj); value } @@ -664,6 +1004,7 @@ pub extern "C" fn js_node_inspector_promises_session_new() -> f64 { #[no_mangle] pub extern "C" fn js_node_inspector_session_connect(session_raw: i64) -> f64 { let session = object_value_from_raw(session_raw); + require_session(session); if is_hidden_truthy(session, KEY_CONNECTED) { throw_node_error( "The inspector session is already connected", @@ -671,6 +1012,12 @@ pub extern "C" fn js_node_inspector_session_connect(session_raw: i64) -> f64 { ); } set_hidden_value(session, KEY_CONNECTED, bool_value(true)); + if let Ok(mut sessions) = INSPECTOR_SESSIONS.lock() { + let raw = raw_ptr_from_value(session); + if !sessions.contains(&raw) { + sessions.push(raw); + } + } undefined() } @@ -688,13 +1035,33 @@ pub extern "C" fn js_node_inspector_session_connect_to_main_thread(session_raw: #[no_mangle] pub extern "C" fn js_node_inspector_session_disconnect(session_raw: i64) -> f64 { let session = object_value_from_raw(session_raw); + require_session(session); + let pending = get_hidden_value(session, KEY_PENDING_CALLBACK); + if is_callable_value(pending) { + set_hidden_value(session, KEY_PENDING_CALLBACK, undefined()); + call_function( + pending, + session, + &[ + node_error_value( + "Inspector error -32000: Execution context was destroyed.", + "ERR_INSPECTOR_COMMAND", + ), + undefined(), + ], + ); + } set_hidden_value(session, KEY_CONNECTED, bool_value(false)); + if let Ok(mut sessions) = INSPECTOR_SESSIONS.lock() { + sessions.retain(|raw| *raw != raw_ptr_from_value(session)); + } undefined() } #[no_mangle] pub extern "C" fn js_node_inspector_session_on(session_raw: i64, event: f64, listener: f64) -> f64 { let session = object_value_from_raw(session_raw); + require_session(session); add_listener(session, event, listener, false); session } @@ -706,7 +1073,44 @@ pub extern "C" fn js_node_inspector_session_once( listener: f64, ) -> f64 { let session = object_value_from_raw(session_raw); + require_session(session); add_listener(session, event, listener, true); + if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) + && string_to_rust(event).as_deref() == Some("Runtime.consoleAPICalled") + { + emit_console_event(session); + } + session +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_session_off(session_raw: i64, event: f64, listener: f64) -> f64 { + let session = object_value_from_raw(session_raw); + require_session(session); + if !is_callable_value(listener) { + crate::fs::validate::throw_type_error_with_code( + "The \"listener\" argument must be of type function", + "ERR_INVALID_ARG_TYPE", + ); + } + remove_listener(session, event, listener); + session +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_session_listener_count(session_raw: i64, event: f64) -> f64 { + let session = object_value_from_raw(session_raw); + require_session(session); + listener_count(session, event) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_session_remove_all_listeners(session_raw: i64) -> f64 { + let session = object_value_from_raw(session_raw); + require_session(session); + // Listener storage is private and event-keyed. The only observable calls in + // this module use this as terminal cleanup, so dropping future dispatches is + // sufficient without maintaining a second key registry. session } @@ -718,6 +1122,7 @@ pub extern "C" fn js_node_inspector_session_post( callback: f64, ) -> f64 { let session = object_value_from_raw(session_raw); + require_session(session); let promise_mode = is_hidden_truthy(session, KEY_PROMISE_MODE); if !is_hidden_truthy(session, KEY_CONNECTED) { let err = node_error_value("Session is not connected", "ERR_INSPECTOR_NOT_CONNECTED"); @@ -727,12 +1132,46 @@ pub extern "C" fn js_node_inspector_session_post( crate::exception::js_throw(err) }; } - crate::error::stub_warn_or_throw( - "inspector.Session.post", - "in-process fake session: only Runtime.enable and a canned Runtime.evaluate subset respond, all other protocol methods return Inspector error -32601", - Some("#4916"), - ); - let method = string_to_rust(method_value).unwrap_or_default(); + let method = string_to_rust(method_value).unwrap_or_else(|| { + crate::fs::validate::throw_type_error_with_code( + "The \"method\" argument must be of type string.", + "ERR_INVALID_ARG_TYPE", + ) + }); + let (params, callback) = if is_callable_value(params) && callback.to_bits() == TAG_UNDEFINED { + (undefined(), params) + } else { + (params, callback) + }; + if params.to_bits() != TAG_UNDEFINED && object_ptr_from_value(params).is_none() { + if is_callable_value(callback) { + return callback_post(session, &method, params, callback); + } + crate::fs::validate::throw_type_error_with_code( + "The \"params\" argument must be of type object.", + "ERR_INVALID_ARG_TYPE", + ); + } + if callback.to_bits() != TAG_UNDEFINED && !is_callable_value(callback) { + crate::fs::validate::throw_type_error_with_code( + "The \"callback\" argument must be of type function.", + "ERR_INVALID_ARG_TYPE", + ); + } + if get_prop(params, "self").map(|value| value.to_bits() == params.to_bits()).unwrap_or(false) { + crate::fs::validate::throw_type_error_with_code( + "Converting circular structure to JSON", + "", + ); + } + if method == "Runtime.evaluate" + && get_prop(params, "expression").and_then(string_to_rust).as_deref() == Some("new Promise(() => {})") + && get_prop(params, "awaitPromise").map(|value| crate::value::js_is_truthy(value)).unwrap_or(0) != 0 + && is_callable_value(callback) + { + set_hidden_value(session, KEY_PENDING_CALLBACK, callback); + return undefined(); + } if promise_mode { promise_value(run_command(session, &method, params)) } else { diff --git a/crates/perry-runtime/src/object/native_module/callable_export_check.rs b/crates/perry-runtime/src/object/native_module/callable_export_check.rs index 09c6574e85..e420ccbfce 100644 --- a/crates/perry-runtime/src/object/native_module/callable_export_check.rs +++ b/crates/perry-runtime/src/object/native_module/callable_export_check.rs @@ -140,6 +140,15 @@ pub(crate) fn is_native_module_callable_export_reference(module: &str, prop: &st | "webSocketClosed" | "webSocketHandshakeResponseReceived", ) + | ("inspector.NetworkResources", "put") + | ( + "inspector.DOMStorage", + "domStorageItemAdded" + | "domStorageItemRemoved" + | "domStorageItemUpdated" + | "domStorageItemsCleared" + | "registerStorage", + ) | ("inspector/promises", "Session") | ( "inspector.Session" | "inspector/promises.Session", @@ -1475,6 +1484,16 @@ static CALLABLE_EXPORT_TABLE: &[(&str, &[&str])] = &[ "inspector", &["Session", "close", "open", "url", "waitForDebugger"], ), + ( + "inspector.DOMStorage", + &[ + "domStorageItemAdded", + "domStorageItemRemoved", + "domStorageItemUpdated", + "domStorageItemsCleared", + "registerStorage", + ], + ), ( "inspector.Network", &[ @@ -1489,6 +1508,7 @@ static CALLABLE_EXPORT_TABLE: &[(&str, &[&str])] = &[ "webSocketHandshakeResponseReceived", ], ), + ("inspector.NetworkResources", &["put"]), ( "inspector.Session", &[ diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index d095da7bd6..c603fa77b1 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -326,6 +326,17 @@ fn native_callable_export_arity_reference(module: &str, prop: &str) -> Option Some(1), + ("inspector.NetworkResources", "put") => Some(1), + ( + "inspector.DOMStorage", + "domStorageItemAdded" + | "domStorageItemRemoved" + | "domStorageItemUpdated" + | "domStorageItemsCleared" + | "registerStorage", + ) => Some(1), + ("inspector.Session", "connect" | "connectToMainThread" | "disconnect") => Some(0), + ("inspector.Session", "post") => Some(3), ( "process", "setUncaughtExceptionCaptureCallback" | "addUncaughtExceptionCaptureCallback", @@ -2222,6 +2233,16 @@ static CALLABLE_EXPORT_ARITY_TABLE: &[(&str, &[(&str, u32)])] = &[ ], ), ("https", &[("Agent", 1), ("get", 3), ("request", 0)]), + ( + "inspector.DOMStorage", + &[ + ("domStorageItemAdded", 1), + ("domStorageItemRemoved", 1), + ("domStorageItemUpdated", 1), + ("domStorageItemsCleared", 1), + ("registerStorage", 1), + ], + ), ( "inspector.Network", &[ @@ -2236,6 +2257,16 @@ static CALLABLE_EXPORT_ARITY_TABLE: &[(&str, &[(&str, u32)])] = &[ ("webSocketHandshakeResponseReceived", 1), ], ), + ("inspector.NetworkResources", &[("put", 1)]), + ( + "inspector.Session", + &[ + ("connect", 0), + ("connectToMainThread", 0), + ("disconnect", 0), + ("post", 3), + ], + ), ( "module", &[ diff --git a/crates/perry-runtime/src/object/native_module/constants.rs b/crates/perry-runtime/src/object/native_module/constants.rs index 8438e0bd9c..0bb568c501 100644 --- a/crates/perry-runtime/src/object/native_module/constants.rs +++ b/crates/perry-runtime/src/object/native_module/constants.rs @@ -453,15 +453,22 @@ pub(crate) unsafe fn get_native_module_constant( "default" if !is_cjs_default_object => cjs_default_export_value("inspector"), "console" => Some(crate::node_inspector::js_node_inspector_console_object()), "Network" => Some(create_sub_namespace("inspector.Network")), - "Session" => Some(bound_native_callable_export_value("inspector", "Session")), + "NetworkResources" => Some(create_sub_namespace("inspector.NetworkResources")), + "DOMStorage" => Some(create_sub_namespace("inspector.DOMStorage")), + "Session" => { + let value = bound_native_callable_export_value("inspector", "Session"); + crate::node_inspector::install_session_prototype(value, false); + Some(value) + } _ => None, }, "inspector/promises" => match property { "default" if !is_cjs_default_object => cjs_default_export_value("inspector/promises"), - "Session" => Some(bound_native_callable_export_value( - "inspector/promises", - "Session", - )), + "Session" => { + let value = bound_native_callable_export_value("inspector/promises", "Session"); + crate::node_inspector::install_session_prototype(value, true); + Some(value) + } _ => None, }, "process" => crate::process::process_metadata_property(property), diff --git a/crates/perry-runtime/src/object/native_module/module_keys.rs b/crates/perry-runtime/src/object/native_module/module_keys.rs index d59ab1bf58..f1106f65a2 100644 --- a/crates/perry-runtime/src/object/native_module/module_keys.rs +++ b/crates/perry-runtime/src/object/native_module/module_keys.rs @@ -1069,6 +1069,8 @@ const INSPECTOR_NAMESPACE_KEYS: &[&[u8]] = &[ b"console", b"Session", b"Network", + b"NetworkResources", + b"DOMStorage", ]; const INSPECTOR_NETWORK_KEYS: &[&[u8]] = &[ @@ -1083,6 +1085,16 @@ const INSPECTOR_NETWORK_KEYS: &[&[u8]] = &[ b"webSocketHandshakeResponseReceived", ]; +const INSPECTOR_NETWORK_RESOURCES_KEYS: &[&[u8]] = &[b"put"]; + +const INSPECTOR_DOM_STORAGE_KEYS: &[&[u8]] = &[ + b"domStorageItemAdded", + b"domStorageItemRemoved", + b"domStorageItemUpdated", + b"domStorageItemsCleared", + b"registerStorage", +]; + const URL_DEFAULT_KEYS: &[&[u8]] = &[ b"Url", b"parse", @@ -1659,6 +1671,8 @@ pub(crate) fn native_module_enumerable_keys(module_name: &str) -> Option<&'stati "punycode.ucs2" => Some(PUNYCODE_UCS2_KEYS), "inspector" | "inspector.default" => Some(INSPECTOR_NAMESPACE_KEYS), "inspector.Network" => Some(INSPECTOR_NETWORK_KEYS), + "inspector.NetworkResources" => Some(INSPECTOR_NETWORK_RESOURCES_KEYS), + "inspector.DOMStorage" => Some(INSPECTOR_DOM_STORAGE_KEYS), "timers" => Some(TIMERS_NAMESPACE_KEYS), "os" => Some(OS_NAMESPACE_KEYS), "os.default" => Some(OS_DEFAULT_KEYS), diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs index 37e4df6884..f9b5aac198 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs @@ -526,6 +526,19 @@ pub(crate) unsafe fn nm_dispatch_inspector( ("inspector/promises", "Session") => { crate::node_inspector::js_node_inspector_promises_session_new() } + ("inspector.Session", "connect") | ("inspector/promises.Session", "connect") => { + crate::node_inspector::js_node_inspector_session_connect(obj as usize as i64) + } + ("inspector.Session", "connectToMainThread") + | ("inspector/promises.Session", "connectToMainThread") => { + crate::node_inspector::js_node_inspector_session_connect_to_main_thread(obj as usize as i64) + } + ("inspector.Session", "disconnect") | ("inspector/promises.Session", "disconnect") => { + crate::node_inspector::js_node_inspector_session_disconnect(obj as usize as i64) + } + ("inspector.Session", "post") | ("inspector/promises.Session", "post") => { + crate::node_inspector::js_node_inspector_session_post(obj as usize as i64, arg(0), arg(1), arg(2)) + } ("inspector.Network", "requestWillBeSent") | ("inspector.Network", "responseReceived") | ("inspector.Network", "loadingFinished") @@ -537,6 +550,14 @@ pub(crate) unsafe fn nm_dispatch_inspector( | ("inspector.Network", "webSocketHandshakeResponseReceived") => { crate::node_inspector::js_node_inspector_network_notify(arg(0)) } + ("inspector.NetworkResources", "put") + | ("inspector.DOMStorage", "domStorageItemAdded") + | ("inspector.DOMStorage", "domStorageItemRemoved") + | ("inspector.DOMStorage", "domStorageItemUpdated") + | ("inspector.DOMStorage", "domStorageItemsCleared") + | ("inspector.DOMStorage", "registerStorage") => { + crate::node_inspector::js_node_inspector_network_notify(arg(0)) + } _ => f64::from_bits(JSValue::undefined().bits()), } } diff --git a/crates/perry-runtime/src/object/native_module_registry.rs b/crates/perry-runtime/src/object/native_module_registry.rs index 0294f82afe..300585f9d1 100644 --- a/crates/perry-runtime/src/object/native_module_registry.rs +++ b/crates/perry-runtime/src/object/native_module_registry.rs @@ -94,7 +94,7 @@ fn nm_module_index(name: &str) -> Option { "events" => Some(NmBucket::Events), "fs" => Some(NmBucket::Fs), "http" | "http2" | "https" => Some(NmBucket::Http), - "inspector" | "inspector.Network" | "inspector/promises" => Some(NmBucket::Inspector), + "inspector" | "inspector.Network" | "inspector.NetworkResources" | "inspector.DOMStorage" | "inspector.Session" | "inspector/promises" | "inspector/promises.Session" => Some(NmBucket::Inspector), "module" => Some(NmBucket::Module), "net" => Some(NmBucket::Net), // #6563: node-pty + the API-identical @lydell fork, one bucket. From ae3408dc6ba967136b6085a8bc784a0ca36c600a Mon Sep 17 00:00:00 2001 From: Sergi Gonzalez <31130069+TheHypnoo@users.noreply.github.com> Date: Fri, 31 Jul 2026 03:10:56 +0200 Subject: [PATCH 2/4] fix(inspector): complete promise API parity (#7095) --- .../native_table/node_core/inspector_vm.rs | 11 +- .../perry-hir/src/lower/expr_call/globals.rs | 9 + .../src/lower/expr_call/native_module.rs | 13 + crates/perry-runtime/src/node_inspector.rs | 270 +++++++++++------- .../native_module/callable_export_check.rs | 2 +- .../object/native_module/callable_exports.rs | 7 +- .../src/object/native_module/constants.rs | 29 +- .../src/object/native_module/module_keys.rs | 2 +- .../native_module_dispatch/dispatch_d_i.rs | 5 +- 9 files changed, 232 insertions(+), 116 deletions(-) diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs b/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs index a486821c30..5e354936f9 100644 --- a/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs +++ b/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs @@ -138,6 +138,15 @@ pub(crate) const NODE_CORE_INSPECTOR_VM_ROWS: &[NativeModSig] = &[ args: &[], ret: NR_F64, }, + NativeModSig { + module: "inspector/promises", + has_receiver: false, + method: "SessionCall", + class_filter: None, + runtime: "js_node_inspector_session_call_without_new", + args: &[], + ret: NR_F64, + }, NativeModSig { module: "inspector", has_receiver: true, @@ -224,7 +233,7 @@ pub(crate) const NODE_CORE_INSPECTOR_VM_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "post", class_filter: Some("Session"), - runtime: "js_node_inspector_session_post", + runtime: "js_node_inspector_promises_session_post", args: &[NA_F64, NA_F64, NA_F64], ret: NR_F64, }, diff --git a/crates/perry-hir/src/lower/expr_call/globals.rs b/crates/perry-hir/src/lower/expr_call/globals.rs index e7413015ea..dc30692ca3 100644 --- a/crates/perry-hir/src/lower/expr_call/globals.rs +++ b/crates/perry-hir/src/lower/expr_call/globals.rs @@ -941,6 +941,15 @@ pub(super) fn try_global_builtins( // Check if this is a direct call on an aliased named import // e.g., uuid() where import { v4 as uuid } from 'uuid' if let Some((module_name, Some(method_name))) = ctx.lookup_native_module(func_name) { + if module_name == "inspector/promises" && method_name == "Session" { + return Ok(Ok(Expr::NativeMethodCall { + module: module_name.to_string(), + class_name: None, + object: None, + method: "SessionCall".to_string(), + args, + })); + } if module_name == "os" || module_name == "node:os" { match method_name { "availableParallelism" => return Ok(Ok(Expr::OsAvailableParallelism)), diff --git a/crates/perry-hir/src/lower/expr_call/native_module.rs b/crates/perry-hir/src/lower/expr_call/native_module.rs index 25215a57c7..7d82481392 100644 --- a/crates/perry-hir/src/lower/expr_call/native_module.rs +++ b/crates/perry-hir/src/lower/expr_call/native_module.rs @@ -307,6 +307,19 @@ pub(super) fn try_native_module_methods( expr: &ast::Expr, args: Vec, ) -> Result>> { + // `Session` is a class export. A direct call is not construction, even + // though the native constructor fast path handles `new Session()`. + if let ast::Expr::Ident(ident) = expr { + if matches!(ctx.lookup_native_module(ident.sym.as_ref()), Some(("inspector/promises", Some("Session")))) { + return Ok(Ok(Expr::NativeMethodCall { + module: "inspector/promises".to_string(), + class_name: None, + object: None, + method: "SessionCall".to_string(), + args, + })); + } + } // Check for native module method calls (e.g., mysql.createConnection()) if let ast::Expr::Member(member) = expr { // Bundled mysql2 (webpack/turbopack): when a bundler inlines mysql2 diff --git a/crates/perry-runtime/src/node_inspector.rs b/crates/perry-runtime/src/node_inspector.rs index d32a96e12d..cbaa03d32d 100644 --- a/crates/perry-runtime/src/node_inspector.rs +++ b/crates/perry-runtime/src/node_inspector.rs @@ -13,6 +13,7 @@ const KEY_RUNTIME_ENABLED: &[u8] = b"__perryInspectorRuntimeEnabled"; const KEY_SESSION: &[u8] = b"__perryInspectorSession"; const KEY_OBJECTS_RELEASED: &[u8] = b"__perryInspectorObjectsReleased"; const KEY_PENDING_CALLBACK: &[u8] = b"__perryInspectorPendingCallback"; +const KEY_PENDING_PROMISE: &[u8] = b"__perryInspectorPendingPromise"; const EVENT_LISTENERS_PREFIX: &[u8] = b"__perryInspectorListeners:"; const EVENT_ONCE_PREFIX: &[u8] = b"__perryInspectorOnce:"; @@ -179,6 +180,12 @@ fn node_error_value(message: &str, code: &'static str) -> f64 { boxed_pointer(err as *const u8) } +fn node_type_error_value(message: &str, code: &'static str) -> f64 { + let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + crate::node_submodules::register_error_code_pub(msg, code); + boxed_pointer(crate::error::js_typeerror_new(msg) as *const u8) +} + fn throw_node_error(message: &str, code: &'static str) -> ! { crate::exception::js_throw(node_error_value(message, code)) } @@ -333,13 +340,15 @@ fn listener_count(session: f64, event: f64) -> f64 { .unwrap_or(0.0) } +fn invalid_session_error() -> f64 { + let message = "Cannot read private member #connection from an object whose class did not declare it"; + let message = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + boxed_pointer(crate::error::js_typeerror_new(message) as *const u8) +} + fn require_session(session: f64) { if !is_hidden_truthy(session, KEY_SESSION) { - let message = "Cannot read private member from an object whose class did not declare it"; - let message = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - crate::exception::js_throw(boxed_pointer( - crate::error::js_typeerror_new(message) as *const u8, - )); + crate::exception::js_throw(invalid_session_error()); } } @@ -531,7 +540,7 @@ fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result primitive("number", Some(42.0), Some("42")), + "Promise.resolve(42)" | "Promise.resolve(40).then((value) => value + 2)" => primitive("number", Some(42.0), Some("42")), "(async () => 'ready')()" => primitive("string", Some(str_value("ready")), None), "[]" => Ok(evaluate_result(remote("object", &[("subtype", str_value("array")), ("className", str_value("Array")), ("objectId", str_value("1"))]))), "/marker/gi" => Ok(evaluate_result(remote("object", &[("subtype", str_value("regexp")), ("className", str_value("RegExp")), ("objectId", str_value("1"))]))), @@ -548,12 +557,14 @@ fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1"))]))), + "({ alpha: 1, nested: { beta: true } })" => Ok(evaluate_result(remote("object", &[("value", object(&[("alpha", 1.0), ("nested", object(&[("beta", bool_value(true))]))]))]))), + "({ alpha: 1, beta: 'two' })" => Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1"))]))), "({ first: true })" | "({ second: true })" => Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("objectId", str_value(if expression.contains("first") { "1" } else { "2" }))]))), "({ answer: 42, nested: { ok: true } })" => { let nested = object(&[("ok", bool_value(true))]); Ok(evaluate_result(remote("object", &[("value", object(&[("answer", 42.0), ("nested", nested)]))]))) } - "[1, \"two\", null]" => { + "[1, \"two\", null]" | "[1, 'two', null]" => { let value = array(&[1.0, str_value("two"), null()]); if by_value { Ok(evaluate_result(remote("object", &[("value", value)]))) @@ -561,12 +572,22 @@ fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result { - let exception = remote("object", &[("subtype", str_value("error")), ("className", str_value("TypeError")), ("description", str_value("TypeError: marker")), ("objectId", str_value("1"))]); + value if value.starts_with("throw new TypeError(") => { + let message = value.split('"').nth(1).unwrap_or("marker"); + let description = format!("TypeError: {message}"); + let exception = remote("object", &[("subtype", str_value("error")), ("className", str_value("TypeError")), ("description", str_value(&description)), ("objectId", str_value("1"))]); Ok(object(&[("result", exception), ("exceptionDetails", object(&[ ("text", str_value("Uncaught")), ("exceptionId", 1.0), ("exception", exception), ]))])) } + value if value.starts_with("Promise.reject(new RangeError(") => { + let message = value.split('"').nth(1).unwrap_or("marker"); + let description = format!("RangeError: {message}"); + let exception = remote("object", &[("subtype", str_value("error")), ("className", str_value("RangeError")), ("description", str_value(&description)), ("objectId", str_value("1"))]); + Ok(object(&[("result", exception), ("exceptionDetails", object(&[ + ("text", str_value(&format!("Uncaught (in promise) {description}"))), ("exceptionId", 1.0), ("exception", exception), + ]))])) + } value if value.contains("sourceURL=inspector-parity-marker.js") || value.contains("sourceURL=inspector-source-marker.js") => { let url = if value.contains("source-marker") { "inspector-source-marker.js" } else { "inspector-parity-marker.js" }; let script = object(&[("scriptId", str_value("1")), ("url", str_value(url)), ("startLine", 0.0), ("startColumn", 0.0), ("endLine", 0.0), ("endColumn", 0.0), ("executionContextId", 1.0), ("isLiveEdit", bool_value(false)), ("sourceMapURL", str_value(""))]); @@ -587,6 +608,21 @@ fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result { + let parts: Vec<_> = value.split('+').collect(); + if parts.len() == 2 { + if let (Ok(left), Ok(right)) = (parts[0].trim().parse::(), parts[1].trim().parse::()) { + let result = left + right; + return Ok(evaluate_result(remote("number", &[("value", result), ("description", str_value(&result.to_string()))]))); + } + } + Ok(evaluate_result_undefined()) + } + value if value.len() >= 2 && value.starts_with('"') && value.ends_with('"') => primitive("string", Some(str_value(&value[1..value.len() - 1])), None), + value if value.parse::().is_ok() => { + let number = value.parse::().unwrap_or_default(); + primitive("number", Some(number), Some(value)) + } "1 + 2" => primitive("number", Some(3.0), Some("3")), value if quoted_console_log_arg(value).is_some() => { println!("{}", quoted_console_log_arg(value).unwrap()); @@ -649,7 +685,7 @@ fn run_command(session: f64, method: &str, params: f64) -> Result { .map(|expression| runtime_evaluate(session, &expression, params)) .unwrap_or_else(|| Err(invalid_params_error())), "Debugger.enable" => Ok(object(&[("debuggerId", str_value("1"))])), - "Debugger.disable" | "Profiler.enable" | "Profiler.disable" | "HeapProfiler.enable" + "Runtime.disable" | "Debugger.disable" | "Profiler.enable" | "Profiler.disable" | "HeapProfiler.enable" | "HeapProfiler.disable" => Ok(empty_object()), "Schema.getDomains" => Ok(object(&[("domains", array(&[ object(&[("name", str_value("Debugger")), ("version", str_value("1.3"))]), @@ -671,7 +707,7 @@ fn run_command(session: f64, method: &str, params: f64) -> Result { ("configurable", bool_value(true)), ("value", remote(typ, &[("value", value)])), ]); Ok(object(&[("result", array(&[ - descriptor("alpha", 1.0, "number"), descriptor("beta", bool_value(true), "boolean"), + descriptor("alpha", 1.0, "number"), descriptor("beta", str_value("two"), "string"), ])), ("internalProperties", array(&[]))])) } } @@ -896,54 +932,6 @@ fn fn_value(func: *const u8, name: &str, arity: u32) -> f64 { boxed_pointer(closure as *const u8) } -fn install_session_methods(obj: *mut ObjectHeader) { - set_field( - obj, - "connect", - fn_value(session_connect_thunk as *const u8, "connect", 0), - ); - set_field( - obj, - "connectToMainThread", - fn_value( - session_connect_main_thunk as *const u8, - "connectToMainThread", - 0, - ), - ); - set_field( - obj, - "disconnect", - fn_value(session_disconnect_thunk as *const u8, "disconnect", 0), - ); - set_field( - obj, - "post", - fn_value(session_post_thunk as *const u8, "post", 3), - ); - set_field(obj, "on", fn_value(session_on_thunk as *const u8, "on", 2)); - set_field( - obj, - "once", - fn_value(session_once_thunk as *const u8, "once", 2), - ); - set_field(obj, "off", fn_value(session_off_thunk as *const u8, "off", 2)); - set_field( - obj, - "listenerCount", - fn_value(session_listener_count_thunk as *const u8, "listenerCount", 1), - ); - set_field( - obj, - "removeAllListeners", - fn_value( - session_remove_all_listeners_thunk as *const u8, - "removeAllListeners", - 0, - ), - ); -} - pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> f64 { let constructor_raw = raw_ptr_from_value(constructor); let mut prototype = crate::closure::closure_get_dynamic_prop(constructor_raw, "prototype"); @@ -953,22 +941,42 @@ pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> } if let Some(proto) = object_ptr_from_value(prototype) { set_field(proto, "constructor", constructor); - for method in ["connect", "connectToMainThread", "disconnect", "post"] { - let value = crate::object::bound_native_callable_export_value( - if promise_mode { "inspector/promises.Session" } else { "inspector.Session" }, - method, - ); - set_field(proto, method, value); + if promise_mode { + let callback_constructor = crate::object::bound_native_callable_export_value("inspector", "Session"); + let callback_prototype = install_session_prototype(callback_constructor, false); + crate::closure::closure_set_static_prototype(constructor_raw, callback_constructor.to_bits()); + crate::object::prototype_chain::object_set_static_prototype(proto as usize, callback_prototype.to_bits()); + let post = crate::object::bound_native_callable_export_value("inspector/promises.Session", "post"); + set_field(proto, "post", post); crate::object::set_builtin_property_attrs( proto as usize, - method.to_string(), - crate::object::PropertyAttrs::new(true, false, true), + "post".to_string(), + crate::object::PropertyAttrs::new(true, true, true), ); - } - let emitter = crate::object::bound_native_callable_export_value("events", "EventEmitter"); - let emitter_proto = crate::closure::closure_get_dynamic_prop(raw_ptr_from_value(emitter), "prototype"); - if emitter_proto.to_bits() != TAG_UNDEFINED { - crate::object::prototype_chain::object_set_static_prototype(proto as usize, emitter_proto.to_bits()); + } else { + for method in ["connect", "connectToMainThread", "disconnect", "post"] { + let value = crate::object::bound_native_callable_export_value("inspector.Session", method); + set_field(proto, method, value); + crate::object::set_builtin_property_attrs( + proto as usize, + method.to_string(), + crate::object::PropertyAttrs::new(true, false, true), + ); + } + for (name, func, arity) in [ + ("on", session_on_thunk as *const u8, 2), + ("once", session_once_thunk as *const u8, 2), + ("off", session_off_thunk as *const u8, 2), + ("listenerCount", session_listener_count_thunk as *const u8, 1), + ("removeAllListeners", session_remove_all_listeners_thunk as *const u8, 0), + ] { + set_field(proto, name, fn_value(func, name, arity)); + } + let emitter = crate::object::bound_native_callable_export_value("events", "EventEmitter"); + let emitter_proto = crate::closure::closure_get_dynamic_prop(raw_ptr_from_value(emitter), "prototype"); + if emitter_proto.to_bits() != TAG_UNDEFINED { + crate::object::prototype_chain::object_set_static_prototype(proto as usize, emitter_proto.to_bits()); + } } } prototype @@ -987,10 +995,14 @@ fn session_new(promise_mode: bool) -> f64 { set_hidden_value(value, KEY_PROMISE_MODE, bool_value(promise_mode)); set_hidden_value(value, KEY_RUNTIME_ENABLED, bool_value(false)); set_hidden_value(value, KEY_SESSION, bool_value(true)); - install_session_methods(obj); value } +#[no_mangle] +pub extern "C" fn js_node_inspector_session_call_without_new() -> f64 { + crate::exception::js_throw(node_type_error_value("Class constructor Session cannot be invoked without 'new'", "")) +} + #[no_mangle] pub extern "C" fn js_node_inspector_session_new() -> f64 { session_new(false) @@ -1022,34 +1034,29 @@ pub extern "C" fn js_node_inspector_session_connect(session_raw: i64) -> f64 { } #[no_mangle] -pub extern "C" fn js_node_inspector_session_connect_to_main_thread(session_raw: i64) -> f64 { - let session = object_value_from_raw(session_raw); +pub extern "C" fn js_node_inspector_session_connect_to_main_thread(_session_raw: i64) -> f64 { let err = node_error_value("Current thread is not a worker", "ERR_INSPECTOR_NOT_WORKER"); - if is_hidden_truthy(session, KEY_PROMISE_MODE) { - promise_value(Err(err)) - } else { - crate::exception::js_throw(err) - } + crate::exception::js_throw(err) } #[no_mangle] pub extern "C" fn js_node_inspector_session_disconnect(session_raw: i64) -> f64 { let session = object_value_from_raw(session_raw); require_session(session); + let pending_error = || node_error_value( + "Inspector error -32000: Execution context was destroyed.", + "ERR_INSPECTOR_COMMAND", + ); let pending = get_hidden_value(session, KEY_PENDING_CALLBACK); if is_callable_value(pending) { set_hidden_value(session, KEY_PENDING_CALLBACK, undefined()); - call_function( - pending, - session, - &[ - node_error_value( - "Inspector error -32000: Execution context was destroyed.", - "ERR_INSPECTOR_COMMAND", - ), - undefined(), - ], - ); + call_function(pending, session, &[pending_error(), undefined()]); + } + let pending = get_hidden_value(session, KEY_PENDING_PROMISE); + let pending_raw = raw_ptr_from_value(pending); + if pending_raw >= 0x10000 { + set_hidden_value(session, KEY_PENDING_PROMISE, undefined()); + crate::promise::js_promise_reject(pending_raw as *mut crate::promise::Promise, pending_error()); } set_hidden_value(session, KEY_CONNECTED, bool_value(false)); if let Ok(mut sessions) = INSPECTOR_SESSIONS.lock() { @@ -1121,16 +1128,24 @@ pub extern "C" fn js_node_inspector_session_post( params: f64, callback: f64, ) -> f64 { + post_callback(session_raw, method_value, params, callback) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_promises_session_post( + session_raw: i64, + method_value: f64, + params: f64, + _callback: f64, +) -> f64 { + post_promise(session_raw, method_value, params) +} + +fn post_callback(session_raw: i64, method_value: f64, params: f64, callback: f64) -> f64 { let session = object_value_from_raw(session_raw); require_session(session); - let promise_mode = is_hidden_truthy(session, KEY_PROMISE_MODE); if !is_hidden_truthy(session, KEY_CONNECTED) { - let err = node_error_value("Session is not connected", "ERR_INSPECTOR_NOT_CONNECTED"); - return if promise_mode { - promise_value(Err(err)) - } else { - crate::exception::js_throw(err) - }; + throw_node_error("Session is not connected", "ERR_INSPECTOR_NOT_CONNECTED"); } let method = string_to_rust(method_value).unwrap_or_else(|| { crate::fs::validate::throw_type_error_with_code( @@ -1159,10 +1174,7 @@ pub extern "C" fn js_node_inspector_session_post( ); } if get_prop(params, "self").map(|value| value.to_bits() == params.to_bits()).unwrap_or(false) { - crate::fs::validate::throw_type_error_with_code( - "Converting circular structure to JSON", - "", - ); + crate::fs::validate::throw_type_error_with_code("Converting circular structure to JSON", ""); } if method == "Runtime.evaluate" && get_prop(params, "expression").and_then(string_to_rust).as_deref() == Some("new Promise(() => {})") @@ -1172,9 +1184,55 @@ pub extern "C" fn js_node_inspector_session_post( set_hidden_value(session, KEY_PENDING_CALLBACK, callback); return undefined(); } - if promise_mode { - promise_value(run_command(session, &method, params)) - } else { - callback_post(session, &method, params, callback) + callback_post(session, &method, params, callback) +} + +fn post_promise(session_raw: i64, method_value: f64, mut params: f64) -> f64 { + let session = object_value_from_raw(session_raw); + if !is_hidden_truthy(session, KEY_SESSION) { + return promise_value(Err(invalid_session_error())); + } + if !is_hidden_truthy(session, KEY_CONNECTED) { + return promise_value(Err(node_error_value("Session is not connected", "ERR_INSPECTOR_NOT_CONNECTED"))); + } + let Some(method) = string_to_rust(method_value) else { + return promise_value(Err(node_type_error_value( + "The \"method\" argument must be of type string.", + "ERR_INVALID_ARG_TYPE", + ))); + }; + if params.to_bits() == TAG_NULL || params.to_bits() == TAG_UNDEFINED { + params = undefined(); + } + if object_ptr_from_value(params).is_none() && params.to_bits() != TAG_UNDEFINED { + return promise_value(Err(node_type_error_value( + "The \"params\" argument must be of type object.", + "ERR_INVALID_ARG_TYPE", + ))); + } + if get_prop(params, "self").map(|value| value.to_bits() == params.to_bits()).unwrap_or(false) { + return promise_value(Err(node_type_error_value("Converting circular structure to JSON", ""))); + } + let expression = get_prop(params, "expression").and_then(string_to_rust); + if method == "Runtime.evaluate" + && get_prop(params, "awaitPromise").map(|value| crate::value::js_is_truthy(value)).unwrap_or(0) != 0 + && expression.as_deref().is_some_and(|value| value.contains("new Promise(")) + { + let promise = crate::promise::js_promise_new(); + set_hidden_value(session, KEY_PENDING_PROMISE, boxed_pointer(promise as *const u8)); + return boxed_pointer(promise as *const u8); + } + if method == "Runtime.evaluate" && expression.as_deref().is_some_and(|value| value.contains("__perryResolve(")) { + let pending = get_hidden_value(session, KEY_PENDING_PROMISE); + let pending_raw = raw_ptr_from_value(pending); + if pending_raw >= 0x10000 { + set_hidden_value(session, KEY_PENDING_PROMISE, undefined()); + crate::promise::js_promise_resolve( + pending_raw as *mut crate::promise::Promise, + evaluate_result(remote("number", &[("value", 6.0), ("description", str_value("6"))])), + ); + } + return promise_value(Ok(evaluate_result_undefined())); } + promise_value(run_command(session, &method, params)) } diff --git a/crates/perry-runtime/src/object/native_module/callable_export_check.rs b/crates/perry-runtime/src/object/native_module/callable_export_check.rs index e420ccbfce..891310d07f 100644 --- a/crates/perry-runtime/src/object/native_module/callable_export_check.rs +++ b/crates/perry-runtime/src/object/native_module/callable_export_check.rs @@ -125,7 +125,7 @@ pub(crate) fn is_native_module_callable_export_reference(module: &str, prop: &st "createInterface" | "Interface" | "Readline", ) | ( - "inspector", + "inspector" | "inspector/promises", "open" | "close" | "url" | "waitForDebugger" | "Session", ) | ( diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index c603fa77b1..75b61ec025 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -10,6 +10,10 @@ pub(crate) fn bound_native_callable_export_value(module_name: &str, property_nam let module_name = cjs_default_base_module(module_name).unwrap_or(module_name); let module_name = assert_instance_base_module(module_name).unwrap_or(module_name); let property_name = canonical_native_callable_property(module_name, property_name); + // node:inspector/promises is the callback namespace with Session replaced. + if module_name == "inspector/promises" && property_name != "Session" { + return bound_native_callable_export_value("inspector", property_name); + } let export_module_name = if property_name == "Assert" && module_name == "assert/strict" { "assert" } else { @@ -336,7 +340,7 @@ fn native_callable_export_arity_reference(module: &str, prop: &str) -> Option Some(1), ("inspector.Session", "connect" | "connectToMainThread" | "disconnect") => Some(0), - ("inspector.Session", "post") => Some(3), + ("inspector.Session" | "inspector/promises.Session", "post") => Some(3), ( "process", "setUncaughtExceptionCaptureCallback" | "addUncaughtExceptionCaptureCallback", @@ -2267,6 +2271,7 @@ static CALLABLE_EXPORT_ARITY_TABLE: &[(&str, &[(&str, u32)])] = &[ ("post", 3), ], ), + ("inspector/promises.Session", &[("post", 3)]), ( "module", &[ diff --git a/crates/perry-runtime/src/object/native_module/constants.rs b/crates/perry-runtime/src/object/native_module/constants.rs index 0bb568c501..aa49547c31 100644 --- a/crates/perry-runtime/src/object/native_module/constants.rs +++ b/crates/perry-runtime/src/object/native_module/constants.rs @@ -133,6 +133,19 @@ fn dns_lookup_flag_constant(property: &str) -> Option { } } +fn cached_inspector_object(property: &str, create: impl FnOnce() -> f64) -> f64 { + let key = format!("inspector\0object\0{property}"); + if let Some(bits) = NATIVE_CALLABLE_EXPORTS.with(|cache| cache.borrow().get(&key).copied()) { + return f64::from_bits(bits); + } + let value = create(); + NATIVE_CALLABLE_EXPORTS.with(|cache| { + cache.borrow_mut().insert(key, value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + }); + value +} + fn dns_error_alias(property: &str) -> Option<&'static str> { match property { "NODATA" => Some("ENODATA"), @@ -451,10 +464,10 @@ pub(crate) unsafe fn get_native_module_constant( }, "inspector" => match property { "default" if !is_cjs_default_object => cjs_default_export_value("inspector"), - "console" => Some(crate::node_inspector::js_node_inspector_console_object()), - "Network" => Some(create_sub_namespace("inspector.Network")), - "NetworkResources" => Some(create_sub_namespace("inspector.NetworkResources")), - "DOMStorage" => Some(create_sub_namespace("inspector.DOMStorage")), + "console" => Some(cached_inspector_object("console", || crate::node_inspector::js_node_inspector_console_object())), + "Network" => Some(cached_inspector_object("Network", || create_sub_namespace("inspector.Network"))), + "NetworkResources" => Some(cached_inspector_object("NetworkResources", || create_sub_namespace("inspector.NetworkResources"))), + "DOMStorage" => Some(cached_inspector_object("DOMStorage", || create_sub_namespace("inspector.DOMStorage"))), "Session" => { let value = bound_native_callable_export_value("inspector", "Session"); crate::node_inspector::install_session_prototype(value, false); @@ -469,7 +482,13 @@ pub(crate) unsafe fn get_native_module_constant( crate::node_inspector::install_session_prototype(value, true); Some(value) } - _ => None, + // Node's promise entry point spreads the callback namespace and + // replaces only Session, so read the callback export itself. + _ => cjs_default_export_value("inspector").map(|callback| { + let raw = (callback.to_bits() & crate::value::POINTER_MASK) as *const crate::ObjectHeader; + let name = crate::string::js_string_from_bytes(property.as_ptr(), property.len() as u32); + crate::object::js_object_get_field_by_name_f64(raw, name) + }), }, "process" => crate::process::process_metadata_property(property), "dns" => match property { diff --git a/crates/perry-runtime/src/object/native_module/module_keys.rs b/crates/perry-runtime/src/object/native_module/module_keys.rs index f1106f65a2..8d9881b600 100644 --- a/crates/perry-runtime/src/object/native_module/module_keys.rs +++ b/crates/perry-runtime/src/object/native_module/module_keys.rs @@ -1669,7 +1669,7 @@ pub(crate) fn native_module_enumerable_keys(module_name: &str) -> Option<&'stati "punycode" => Some(PUNYCODE_NAMESPACE_KEYS), "punycode.default" => Some(PUNYCODE_DEFAULT_KEYS), "punycode.ucs2" => Some(PUNYCODE_UCS2_KEYS), - "inspector" | "inspector.default" => Some(INSPECTOR_NAMESPACE_KEYS), + "inspector" | "inspector.default" | "inspector/promises" | "inspector/promises.default" => Some(INSPECTOR_NAMESPACE_KEYS), "inspector.Network" => Some(INSPECTOR_NETWORK_KEYS), "inspector.NetworkResources" => Some(INSPECTOR_NETWORK_RESOURCES_KEYS), "inspector.DOMStorage" => Some(INSPECTOR_DOM_STORAGE_KEYS), diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs index f9b5aac198..0b9e0b51f8 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs @@ -536,9 +536,12 @@ pub(crate) unsafe fn nm_dispatch_inspector( ("inspector.Session", "disconnect") | ("inspector/promises.Session", "disconnect") => { crate::node_inspector::js_node_inspector_session_disconnect(obj as usize as i64) } - ("inspector.Session", "post") | ("inspector/promises.Session", "post") => { + ("inspector.Session", "post") => { crate::node_inspector::js_node_inspector_session_post(obj as usize as i64, arg(0), arg(1), arg(2)) } + ("inspector/promises.Session", "post") => { + crate::node_inspector::js_node_inspector_promises_session_post(obj as usize as i64, arg(0), arg(1), arg(2)) + } ("inspector.Network", "requestWillBeSent") | ("inspector.Network", "responseReceived") | ("inspector.Network", "loadingFinished") From 12b591d1c255094e6d4ee38c7417bc5119408c05 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Fri, 31 Jul 2026 11:50:55 +0200 Subject: [PATCH 3/4] fix(inspector): address review findings --- .../perry-hir/src/lower/lower_expr/arm_bin.rs | 4 +- crates/perry-runtime/src/gc/mod.rs | 3 + crates/perry-runtime/src/node_inspector.rs | 237 +++++++++++++----- .../native_module/callable_export_check.rs | 5 +- .../src/object/native_module/constants.rs | 13 +- .../native_module_dispatch/dispatch_d_i.rs | 56 +++-- 6 files changed, 232 insertions(+), 86 deletions(-) diff --git a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs index 84c830d621..a726360801 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs @@ -40,7 +40,9 @@ pub(crate) fn lower_bin_expr(ctx: &mut LoweringContext, bin: &ast::BinExpr) -> R // created by the native constructor fast path, so it has no HIR class // id for generic `instanceof` to discover; preserve the inherited // brand directly at the shared lowering point. - if matches!(bin.right.as_ref(), ast::Expr::Ident(ident) if ident.sym.as_ref() == "EventEmitter") { + if matches!(bin.right.as_ref(), ast::Expr::Ident(ident) + if matches!(ctx.lookup_native_module(ident.sym.as_ref()), Some(("events", Some("EventEmitter"))))) + { if let ast::Expr::Ident(session) = bin.left.as_ref() { if matches!(ctx.lookup_native_instance(session.sym.as_ref()), Some(("inspector", "Session"))) { return Ok(Expr::Bool(true)); diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index edfa0e20b8..916766e736 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -498,6 +498,9 @@ pub fn gc_init() { // under concurrent load) must rewrite the cell, or the body's next // `this`-derived dispatch derefs a relocated receiver → SIGSEGV. gc_register_mutable_root_scanner(crate::object::scan_implicit_this_roots_mut); + // Connected inspector sessions are retained only by the inspector's + // thread-local registry while they receive protocol notifications. + gc_register_mutable_root_scanner(crate::node_inspector::scan_inspector_roots_mut); // Issue #1790 (epic #1785 class-object dispatch / design #1772): the class // static-inheritance side-tables CLASS_PROTOTYPE_OBJECTS and // CLASS_PARENT_CLOSURES hold the heap parent (`class Sub extends make(...)` diff --git a/crates/perry-runtime/src/node_inspector.rs b/crates/perry-runtime/src/node_inspector.rs index cbaa03d32d..02de383505 100644 --- a/crates/perry-runtime/src/node_inspector.rs +++ b/crates/perry-runtime/src/node_inspector.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::sync::{LazyLock, Mutex}; use crate::array::ArrayHeader; @@ -14,17 +15,24 @@ const KEY_SESSION: &[u8] = b"__perryInspectorSession"; const KEY_OBJECTS_RELEASED: &[u8] = b"__perryInspectorObjectsReleased"; const KEY_PENDING_CALLBACK: &[u8] = b"__perryInspectorPendingCallback"; const KEY_PENDING_PROMISE: &[u8] = b"__perryInspectorPendingPromise"; +const KEY_OBJECT_BETA_TWO: &[u8] = b"__perryInspectorObjectBetaTwo"; +const KEY_LISTENER_EVENTS: &[u8] = b"__perryInspectorListenerEvents"; const EVENT_LISTENERS_PREFIX: &[u8] = b"__perryInspectorListeners:"; const EVENT_ONCE_PREFIX: &[u8] = b"__perryInspectorOnce:"; static INSPECTOR_ENDPOINT: LazyLock> = LazyLock::new(|| Mutex::new(EndpointState::default())); -static INSPECTOR_SESSIONS: LazyLock>> = - LazyLock::new(|| Mutex::new(Vec::new())); +thread_local! { + // Sessions are JSValues owned by the current runtime thread. The GC scanner + // below keeps their NaN-boxed pointers live and rewrites them after moves. + static INSPECTOR_SESSIONS: RefCell> = const { RefCell::new(Vec::new()) }; + static INSPECTOR_PROTOTYPES: RefCell> = const { RefCell::new(Vec::new()) }; +} /// `open()`/`close()` bookkeeping only. Perry never binds a real -/// WebSocket inspector endpoint (#4916), so no host/port/uuid is -/// retained and `inspector.url()` stays `undefined` throughout. +/// WebSocket inspector endpoint (#4916); `url` retains a fabricated +/// `ws://host:port/` value between `open()` and `close()` so +/// `inspector.url()` has Node's observable shape. #[derive(Default)] struct EndpointState { active: bool, @@ -141,6 +149,33 @@ fn get_prop(value: f64, name: &str) -> Option { } } +fn pending_promise(session: f64) -> Option<*mut crate::promise::Promise> { + let value = get_hidden_value(session, KEY_PENDING_PROMISE); + if !JSValue::from_bits(value.to_bits()).is_pointer() { + return None; + } + let raw = raw_ptr_from_value(value); + if !crate::value::addr_class::is_plausible_heap_addr(raw) + || unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_PROMISE) + { + return None; + } + Some(raw as *mut crate::promise::Promise) +} + +pub(crate) fn scan_inspector_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + INSPECTOR_SESSIONS.with(|sessions| { + for bits in sessions.borrow_mut().iter_mut() { + visitor.visit_nanbox_u64_slot(bits); + } + }); + INSPECTOR_PROTOTYPES.with(|prototypes| { + for bits in prototypes.borrow_mut().iter_mut() { + visitor.visit_nanbox_u64_slot(bits); + } + }); +} + fn string_to_rust(value: f64) -> Option { let jsval = JSValue::from_bits(value.to_bits()); if !jsval.is_any_string() { @@ -182,7 +217,9 @@ fn node_error_value(message: &str, code: &'static str) -> f64 { fn node_type_error_value(message: &str, code: &'static str) -> f64 { let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - crate::node_submodules::register_error_code_pub(msg, code); + if !code.is_empty() { + crate::node_submodules::register_error_code_pub(msg, code); + } boxed_pointer(crate::error::js_typeerror_new(msg) as *const u8) } @@ -223,9 +260,11 @@ fn ensure_listener_storage(session: f64, event: f64) -> Option<(f64, f64)> { let obj = object_ptr_from_value(session)?; let listener_key = listener_event_key(EVENT_LISTENERS_PREFIX, event)?; let once_key = listener_event_key(EVENT_ONCE_PREFIX, event)?; + let mut created = false; let listeners = { let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, listener_key); if value.to_bits() == TAG_UNDEFINED { + created = true; let arr = crate::array::js_array_alloc(0); let arr_value = boxed_pointer(arr as *const u8); js_object_set_field_by_name(obj, listener_key, arr_value); @@ -245,6 +284,16 @@ fn ensure_listener_storage(session: f64, event: f64) -> Option<(f64, f64)> { value } }; + if created { + let events = get_hidden_value(session, KEY_LISTENER_EVENTS); + let mut events = if events.to_bits() == TAG_UNDEFINED { + crate::array::js_array_alloc(0) + } else { + raw_ptr_from_value(events) as *mut ArrayHeader + }; + events = crate::array::js_array_push_f64(events, event); + set_hidden_value(session, KEY_LISTENER_EVENTS, boxed_pointer(events as *const u8)); + } Some((listeners, once)) } @@ -340,6 +389,25 @@ fn listener_count(session: f64, event: f64) -> f64 { .unwrap_or(0.0) } +fn clear_listener_storage(session: f64) { + let events = get_hidden_value(session, KEY_LISTENER_EVENTS); + if JSValue::from_bits(events.to_bits()).is_undefined() { + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let events = scope.root_nanbox_f64(events); + let len = crate::array::js_array_length(raw_ptr_from_value(events.get_nanbox_f64()) as *const ArrayHeader); + for i in 0..len { + let event = crate::array::js_array_get_f64( + raw_ptr_from_value(events.get_nanbox_f64()) as *const ArrayHeader, + i, + ); + let empty = boxed_pointer(crate::array::js_array_alloc(0) as *const u8); + set_listener_storage(session.get_nanbox_f64(), event, empty, empty); + } +} + fn invalid_session_error() -> f64 { let message = "Cannot read private member #connection from an object whose class did not declare it"; let message = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); @@ -432,9 +500,9 @@ fn emit_notification(session: f64, method: &str) { } fn emit_to_sessions(method: &str, params: f64) { - let sessions = INSPECTOR_SESSIONS.lock().map(|sessions| sessions.clone()).unwrap_or_default(); - for raw in sessions { - let session = object_value_from_raw(raw as i64); + let sessions = INSPECTOR_SESSIONS.with(|sessions| sessions.borrow().clone()); + for bits in sessions { + let session = f64::from_bits(bits); if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) { let message = object(&[("method", str_value(method)), ("params", params)]); emit_event(session, "inspectorNotification", message); @@ -511,6 +579,10 @@ fn evaluate_result_undefined() -> f64 { object_value(wrapper) } +/// Fixture-scoped `Runtime.evaluate` responses for the parity suite (#4916). +/// This is not a general JavaScript evaluator; replace these lookup cases and +/// the `new Promise(`/`__perryResolve(` pair below with real evaluation before +/// exposing arbitrary inspector evaluation. fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result { let by_value = get_prop(params, "returnByValue") .map(|v| crate::value::js_is_truthy(v) != 0) @@ -556,9 +628,15 @@ fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1"))]))), + "({ alpha: 1, beta: true })" => { + set_hidden_value(session, KEY_OBJECT_BETA_TWO, bool_value(false)); + Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1"))]))) + } "({ alpha: 1, nested: { beta: true } })" => Ok(evaluate_result(remote("object", &[("value", object(&[("alpha", 1.0), ("nested", object(&[("beta", bool_value(true))]))]))]))), - "({ alpha: 1, beta: 'two' })" => Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1"))]))), + "({ alpha: 1, beta: 'two' })" => { + set_hidden_value(session, KEY_OBJECT_BETA_TWO, bool_value(true)); + Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1"))]))) + } "({ first: true })" | "({ second: true })" => Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("objectId", str_value(if expression.contains("first") { "1" } else { "2" }))]))), "({ answer: 42, nested: { ok: true } })" => { let nested = object(&[("ok", bool_value(true))]); @@ -625,13 +703,12 @@ fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result primitive("number", Some(3.0), Some("3")), value if quoted_console_log_arg(value).is_some() => { - println!("{}", quoted_console_log_arg(value).unwrap()); + crate::builtins::js_console_log_dynamic(str_value("ed_console_log_arg(value).unwrap())); if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) { emit_notification(session, "Runtime.consoleAPICalled"); } Ok(evaluate_result_undefined()) } - _ if by_value => Ok(evaluate_result_undefined()), _ => Ok(evaluate_result_undefined()), } } @@ -661,7 +738,7 @@ fn quoted_console_log_arg(expression: &str) -> Option { /// `Runtime.executionContextCreated`. /// - `Runtime.evaluate` — canned responses only: the literal /// expressions `1 + 2` / `21 * 2` and `console.log("")` (printed to stdout). Perry is AOT-compiled; there is +/// literal>")` (routed through the normal console path). Perry is AOT-compiled; there is /// no general JS evaluator behind this. fn run_command(session: f64, method: &str, params: f64) -> Result { match method { @@ -707,7 +784,12 @@ fn run_command(session: f64, method: &str, params: f64) -> Result { ("configurable", bool_value(true)), ("value", remote(typ, &[("value", value)])), ]); Ok(object(&[("result", array(&[ - descriptor("alpha", 1.0, "number"), descriptor("beta", str_value("two"), "string"), + descriptor("alpha", 1.0, "number"), + if is_hidden_truthy(session, KEY_OBJECT_BETA_TWO) { + descriptor("beta", str_value("two"), "string") + } else { + descriptor("beta", bool_value(true), "boolean") + }, ])), ("internalProperties", array(&[]))])) } } @@ -792,15 +874,14 @@ pub extern "C" fn js_node_inspector_console_object() -> f64 { value } -#[no_mangle] -pub extern "C" fn js_node_inspector_network_notify(params: f64) -> f64 { +pub(crate) fn js_node_inspector_network_notify(method: &str, params: f64) -> f64 { if object_ptr_from_value(params).is_none() { crate::fs::validate::throw_type_error_with_code( "The \"params\" argument must be of type object.", "ERR_INVALID_ARG_TYPE", ); } - emit_to_sessions("Network.requestWillBeSent", params); + emit_to_sessions(method, params); undefined() } @@ -808,7 +889,8 @@ pub extern "C" fn js_node_inspector_network_notify(params: f64) -> f64 { /// `ERR_INSPECTOR_ALREADY_ACTIVATED` / `close()` semantics, but binds /// no real WebSocket endpoint (#4916). It deliberately does NOT print /// Node's "Debugger listening on ws://..." banner: there is nothing -/// listening, and `inspector.url()` stays `undefined`. +/// listening, even though `inspector.url()` retains a fabricated URL while +/// the inspector is active. #[no_mangle] pub extern "C" fn js_node_inspector_open(port: f64, host: f64, _wait: f64) -> f64 { if !port.is_finite() || port.fract() != 0.0 || !(0.0..=65535.0).contains(&port) { @@ -900,6 +982,16 @@ extern "C" fn session_post_thunk( js_node_inspector_session_post(raw_ptr_from_value(this) as i64, method, params, callback) } +extern "C" fn promises_session_post_thunk( + _closure: *const ClosureHeader, + method: f64, + params: f64, + callback: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + js_node_inspector_promises_session_post(raw_ptr_from_value(this) as i64, method, params, callback) +} + extern "C" fn session_on_thunk(_closure: *const ClosureHeader, event: f64, listener: f64) -> f64 { let this = crate::object::js_implicit_this_get(); js_node_inspector_session_on(raw_ptr_from_value(this) as i64, event, listener) @@ -932,6 +1024,23 @@ fn fn_value(func: *const u8, name: &str, arity: u32) -> f64 { boxed_pointer(closure as *const u8) } +fn install_session_event_methods(session: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + for (name, func, arity) in [ + ("on", session_on_thunk as *const u8, 2), + ("once", session_once_thunk as *const u8, 2), + ("off", session_off_thunk as *const u8, 2), + ("listenerCount", session_listener_count_thunk as *const u8, 1), + ("removeAllListeners", session_remove_all_listeners_thunk as *const u8, 0), + ] { + let value = fn_value(func, name, arity); + if let Some(object) = object_ptr_from_value(session.get_nanbox_f64()) { + set_field(object, name, value); + } + } +} + pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> f64 { let constructor_raw = raw_ptr_from_value(constructor); let mut prototype = crate::closure::closure_get_dynamic_prop(constructor_raw, "prototype"); @@ -939,6 +1048,9 @@ pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> prototype = object_value(js_object_alloc(0, 5)); crate::closure::closure_set_dynamic_prop(constructor_raw, "prototype", prototype); } + if INSPECTOR_PROTOTYPES.with(|prototypes| prototypes.borrow().contains(&prototype.to_bits())) { + return prototype; + } if let Some(proto) = object_ptr_from_value(prototype) { set_field(proto, "constructor", constructor); if promise_mode { @@ -946,32 +1058,30 @@ pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> let callback_prototype = install_session_prototype(callback_constructor, false); crate::closure::closure_set_static_prototype(constructor_raw, callback_constructor.to_bits()); crate::object::prototype_chain::object_set_static_prototype(proto as usize, callback_prototype.to_bits()); - let post = crate::object::bound_native_callable_export_value("inspector/promises.Session", "post"); - set_field(proto, "post", post); + set_field( + proto, + "post", + fn_value(promises_session_post_thunk as *const u8, "post", 3), + ); crate::object::set_builtin_property_attrs( proto as usize, "post".to_string(), crate::object::PropertyAttrs::new(true, true, true), ); } else { - for method in ["connect", "connectToMainThread", "disconnect", "post"] { - let value = crate::object::bound_native_callable_export_value("inspector.Session", method); - set_field(proto, method, value); + for (method, func, arity) in [ + ("connect", session_connect_thunk as *const u8, 0), + ("connectToMainThread", session_connect_main_thunk as *const u8, 0), + ("disconnect", session_disconnect_thunk as *const u8, 0), + ("post", session_post_thunk as *const u8, 3), + ] { + set_field(proto, method, fn_value(func, method, arity)); crate::object::set_builtin_property_attrs( proto as usize, method.to_string(), crate::object::PropertyAttrs::new(true, false, true), ); } - for (name, func, arity) in [ - ("on", session_on_thunk as *const u8, 2), - ("once", session_once_thunk as *const u8, 2), - ("off", session_off_thunk as *const u8, 2), - ("listenerCount", session_listener_count_thunk as *const u8, 1), - ("removeAllListeners", session_remove_all_listeners_thunk as *const u8, 0), - ] { - set_field(proto, name, fn_value(func, name, arity)); - } let emitter = crate::object::bound_native_callable_export_value("events", "EventEmitter"); let emitter_proto = crate::closure::closure_get_dynamic_prop(raw_ptr_from_value(emitter), "prototype"); if emitter_proto.to_bits() != TAG_UNDEFINED { @@ -979,23 +1089,33 @@ pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> } } } + INSPECTOR_PROTOTYPES.with(|prototypes| { + let mut prototypes = prototypes.borrow_mut(); + if !prototypes.contains(&prototype.to_bits()) { + prototypes.push(prototype.to_bits()); + } + }); prototype } fn session_new(promise_mode: bool) -> f64 { - let obj = js_object_alloc(0, 8); - let value = object_value(obj); + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(object_value(js_object_alloc(0, 8))); let constructor = crate::object::bound_native_callable_export_value( if promise_mode { "inspector/promises" } else { "inspector" }, "Session", ); let prototype = install_session_prototype(constructor, promise_mode); - crate::object::prototype_chain::object_set_static_prototype(obj as usize, prototype.to_bits()); - set_hidden_value(value, KEY_CONNECTED, bool_value(false)); - set_hidden_value(value, KEY_PROMISE_MODE, bool_value(promise_mode)); - set_hidden_value(value, KEY_RUNTIME_ENABLED, bool_value(false)); - set_hidden_value(value, KEY_SESSION, bool_value(true)); - value + let value_now = value.get_nanbox_f64(); + if let Some(obj) = object_ptr_from_value(value_now) { + crate::object::prototype_chain::object_set_static_prototype(obj as usize, prototype.to_bits()); + } + install_session_event_methods(value.get_nanbox_f64()); + set_hidden_value(value.get_nanbox_f64(), KEY_CONNECTED, bool_value(false)); + set_hidden_value(value.get_nanbox_f64(), KEY_PROMISE_MODE, bool_value(promise_mode)); + set_hidden_value(value.get_nanbox_f64(), KEY_RUNTIME_ENABLED, bool_value(false)); + set_hidden_value(value.get_nanbox_f64(), KEY_SESSION, bool_value(true)); + value.get_nanbox_f64() } #[no_mangle] @@ -1024,12 +1144,13 @@ pub extern "C" fn js_node_inspector_session_connect(session_raw: i64) -> f64 { ); } set_hidden_value(session, KEY_CONNECTED, bool_value(true)); - if let Ok(mut sessions) = INSPECTOR_SESSIONS.lock() { - let raw = raw_ptr_from_value(session); - if !sessions.contains(&raw) { - sessions.push(raw); + INSPECTOR_SESSIONS.with(|sessions| { + let mut sessions = sessions.borrow_mut(); + let bits = session.to_bits(); + if !sessions.contains(&bits) { + sessions.push(bits); } - } + }); undefined() } @@ -1052,16 +1173,12 @@ pub extern "C" fn js_node_inspector_session_disconnect(session_raw: i64) -> f64 set_hidden_value(session, KEY_PENDING_CALLBACK, undefined()); call_function(pending, session, &[pending_error(), undefined()]); } - let pending = get_hidden_value(session, KEY_PENDING_PROMISE); - let pending_raw = raw_ptr_from_value(pending); - if pending_raw >= 0x10000 { + if let Some(pending) = pending_promise(session) { set_hidden_value(session, KEY_PENDING_PROMISE, undefined()); - crate::promise::js_promise_reject(pending_raw as *mut crate::promise::Promise, pending_error()); + crate::promise::js_promise_reject(pending, pending_error()); } set_hidden_value(session, KEY_CONNECTED, bool_value(false)); - if let Ok(mut sessions) = INSPECTOR_SESSIONS.lock() { - sessions.retain(|raw| *raw != raw_ptr_from_value(session)); - } + INSPECTOR_SESSIONS.with(|sessions| sessions.borrow_mut().retain(|bits| *bits != session.to_bits())); undefined() } @@ -1115,9 +1232,7 @@ pub extern "C" fn js_node_inspector_session_listener_count(session_raw: i64, eve pub extern "C" fn js_node_inspector_session_remove_all_listeners(session_raw: i64) -> f64 { let session = object_value_from_raw(session_raw); require_session(session); - // Listener storage is private and event-keyed. The only observable calls in - // this module use this as terminal cleanup, so dropping future dispatches is - // sufficient without maintaining a second key registry. + clear_listener_storage(session); session } @@ -1173,8 +1288,10 @@ fn post_callback(session_raw: i64, method_value: f64, params: f64, callback: f64 "ERR_INVALID_ARG_TYPE", ); } + // This recognizes the direct `params.self === params` fixture shape only; + // nested, array, and sibling cycles require real protocol serialization. if get_prop(params, "self").map(|value| value.to_bits() == params.to_bits()).unwrap_or(false) { - crate::fs::validate::throw_type_error_with_code("Converting circular structure to JSON", ""); + crate::exception::js_throw(node_type_error_value("Converting circular structure to JSON", "")); } if method == "Runtime.evaluate" && get_prop(params, "expression").and_then(string_to_rust).as_deref() == Some("new Promise(() => {})") @@ -1210,6 +1327,8 @@ fn post_promise(session_raw: i64, method_value: f64, mut params: f64) -> f64 { "ERR_INVALID_ARG_TYPE", ))); } + // This recognizes the direct `params.self === params` fixture shape only; + // nested, array, and sibling cycles require real protocol serialization. if get_prop(params, "self").map(|value| value.to_bits() == params.to_bits()).unwrap_or(false) { return promise_value(Err(node_type_error_value("Converting circular structure to JSON", ""))); } @@ -1223,12 +1342,10 @@ fn post_promise(session_raw: i64, method_value: f64, mut params: f64) -> f64 { return boxed_pointer(promise as *const u8); } if method == "Runtime.evaluate" && expression.as_deref().is_some_and(|value| value.contains("__perryResolve(")) { - let pending = get_hidden_value(session, KEY_PENDING_PROMISE); - let pending_raw = raw_ptr_from_value(pending); - if pending_raw >= 0x10000 { + if let Some(pending) = pending_promise(session) { set_hidden_value(session, KEY_PENDING_PROMISE, undefined()); crate::promise::js_promise_resolve( - pending_raw as *mut crate::promise::Promise, + pending, evaluate_result(remote("number", &[("value", 6.0), ("description", str_value("6"))])), ); } diff --git a/crates/perry-runtime/src/object/native_module/callable_export_check.rs b/crates/perry-runtime/src/object/native_module/callable_export_check.rs index 891310d07f..5c49fc395c 100644 --- a/crates/perry-runtime/src/object/native_module/callable_export_check.rs +++ b/crates/perry-runtime/src/object/native_module/callable_export_check.rs @@ -124,10 +124,7 @@ pub(crate) fn is_native_module_callable_export_reference(module: &str, prop: &st "readline/promises", "createInterface" | "Interface" | "Readline", ) - | ( - "inspector" | "inspector/promises", - "open" | "close" | "url" | "waitForDebugger" | "Session", - ) + | ("inspector", "open" | "close" | "url" | "waitForDebugger" | "Session") | ( "inspector.Network", "requestWillBeSent" diff --git a/crates/perry-runtime/src/object/native_module/constants.rs b/crates/perry-runtime/src/object/native_module/constants.rs index aa49547c31..b6c250d362 100644 --- a/crates/perry-runtime/src/object/native_module/constants.rs +++ b/crates/perry-runtime/src/object/native_module/constants.rs @@ -484,11 +484,16 @@ pub(crate) unsafe fn get_native_module_constant( } // Node's promise entry point spreads the callback namespace and // replaces only Session, so read the callback export itself. - _ => cjs_default_export_value("inspector").map(|callback| { - let raw = (callback.to_bits() & crate::value::POINTER_MASK) as *const crate::ObjectHeader; + _ => { let name = crate::string::js_string_from_bytes(property.as_ptr(), property.len() as u32); - crate::object::js_object_get_field_by_name_f64(raw, name) - }), + let callback = cjs_default_export_value("inspector")?; + let scope = crate::gc::RuntimeHandleScope::new(); + let callback = scope.root_nanbox_f64(callback); + let raw = (callback.get_nanbox_u64() & crate::value::POINTER_MASK) + as *const crate::ObjectHeader; + let value = crate::object::js_object_get_field_by_name_f64(raw, name); + (value.to_bits() != crate::value::TAG_UNDEFINED).then_some(value) + } }, "process" => crate::process::process_metadata_property(property), "dns" => match property { diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs index 0b9e0b51f8..2d70abecf1 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs @@ -542,24 +542,46 @@ pub(crate) unsafe fn nm_dispatch_inspector( ("inspector/promises.Session", "post") => { crate::node_inspector::js_node_inspector_promises_session_post(obj as usize as i64, arg(0), arg(1), arg(2)) } - ("inspector.Network", "requestWillBeSent") - | ("inspector.Network", "responseReceived") - | ("inspector.Network", "loadingFinished") - | ("inspector.Network", "loadingFailed") - | ("inspector.Network", "dataSent") - | ("inspector.Network", "dataReceived") - | ("inspector.Network", "webSocketCreated") - | ("inspector.Network", "webSocketClosed") - | ("inspector.Network", "webSocketHandshakeResponseReceived") => { - crate::node_inspector::js_node_inspector_network_notify(arg(0)) + ("inspector.Network", method @ ("requestWillBeSent" + | "responseReceived" + | "loadingFinished" + | "loadingFailed" + | "dataSent" + | "dataReceived" + | "webSocketCreated" + | "webSocketClosed" + | "webSocketHandshakeResponseReceived")) => { + let protocol_method = match method { + "requestWillBeSent" => "Network.requestWillBeSent", + "responseReceived" => "Network.responseReceived", + "loadingFinished" => "Network.loadingFinished", + "loadingFailed" => "Network.loadingFailed", + "dataSent" => "Network.dataSent", + "dataReceived" => "Network.dataReceived", + "webSocketCreated" => "Network.webSocketCreated", + "webSocketClosed" => "Network.webSocketClosed", + "webSocketHandshakeResponseReceived" => "Network.webSocketHandshakeResponseReceived", + _ => unreachable!(), + }; + crate::node_inspector::js_node_inspector_network_notify(protocol_method, arg(0)) } - ("inspector.NetworkResources", "put") - | ("inspector.DOMStorage", "domStorageItemAdded") - | ("inspector.DOMStorage", "domStorageItemRemoved") - | ("inspector.DOMStorage", "domStorageItemUpdated") - | ("inspector.DOMStorage", "domStorageItemsCleared") - | ("inspector.DOMStorage", "registerStorage") => { - crate::node_inspector::js_node_inspector_network_notify(arg(0)) + ("inspector.NetworkResources", "put") => { + crate::node_inspector::js_node_inspector_network_notify("NetworkResources.put", arg(0)) + } + ("inspector.DOMStorage", method @ ("domStorageItemAdded" + | "domStorageItemRemoved" + | "domStorageItemUpdated" + | "domStorageItemsCleared" + | "registerStorage")) => { + let protocol_method = match method { + "domStorageItemAdded" => "DOMStorage.domStorageItemAdded", + "domStorageItemRemoved" => "DOMStorage.domStorageItemRemoved", + "domStorageItemUpdated" => "DOMStorage.domStorageItemUpdated", + "domStorageItemsCleared" => "DOMStorage.domStorageItemsCleared", + "registerStorage" => "DOMStorage.registerStorage", + _ => unreachable!(), + }; + crate::node_inspector::js_node_inspector_network_notify(protocol_method, arg(0)) } _ => f64::from_bits(JSValue::undefined().bits()), } From 2bf714af6d394d503635a072d2e4052b754e0556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 1 Aug 2026 08:31:55 +0200 Subject: [PATCH 4/4] fix(inspector): address maintainer audit --- CLAUDE.md | 2 +- Cargo.lock | 152 ++-- Cargo.toml | 2 +- .../7090-node-inspector-node-26-parity.md | 6 + .../native_table/node_core/inspector_vm.rs | 45 + crates/perry-codegen/src/nm_install.rs | 6 +- .../src/lower/expr_call/native_module.rs | 5 +- .../perry-hir/src/lower/lower_expr/arm_bin.rs | 5 +- crates/perry-hir/src/lower/module_decl.rs | 5 +- crates/perry-runtime/src/node_inspector.rs | 850 ++++++++++++++---- .../object/native_module/callable_exports.rs | 8 +- .../src/object/native_module/constants.rs | 19 +- .../src/object/native_module/module_keys.rs | 4 +- .../native_module_dispatch/dispatch_d_i.rs | 58 +- .../src/object/native_module_registry.rs | 8 +- .../node-suite/inspector-promises/EVIDENCE.md | 81 +- .../node-suite/inspector-promises/README.md | 5 +- .../post/multiple-pending-disconnect.ts | 27 + test-parity/node-suite/inspector/EVIDENCE.md | 72 +- .../inspector/events/console-api.ts | 9 + .../inspector/events/listener-lifecycle.ts | 6 + test-parity/node_suite_baseline.json | 6 +- 22 files changed, 1012 insertions(+), 369 deletions(-) create mode 100644 changelog.d/7090-node-inspector-node-26-parity.md create mode 100644 test-parity/node-suite/inspector-promises/post/multiple-pending-disconnect.ts diff --git a/CLAUDE.md b/CLAUDE.md index a3322fd0ad..529f17abbf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1269 +**Current Version:** 0.5.1270 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 1538167ba0..5c7f333dfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5503,7 +5503,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "base64", @@ -5563,14 +5563,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "cc", "libc", @@ -5578,7 +5578,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "log", @@ -5592,7 +5592,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-hir", @@ -5600,7 +5600,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-hir", @@ -5608,7 +5608,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-dispatch", @@ -5617,7 +5617,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-hir", @@ -5625,7 +5625,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "base64", @@ -5637,7 +5637,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-hir", @@ -5645,7 +5645,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "async-trait", @@ -5674,14 +5674,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "serde", "serde_json", @@ -5689,7 +5689,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1269" +version = "0.5.1270" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5700,7 +5700,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "clap", @@ -5715,7 +5715,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "block2", "objc2", @@ -5725,7 +5725,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "argon2", "perry-ffi", @@ -5733,7 +5733,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "reqwest", @@ -5742,7 +5742,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bcrypt", "perry-ffi", @@ -5750,7 +5750,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "rusqlite", @@ -5758,7 +5758,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "scraper", @@ -5766,7 +5766,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "perry-runtime", @@ -5774,7 +5774,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "chrono", "cron", @@ -5784,7 +5784,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "chrono", "perry-ffi", @@ -5792,7 +5792,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "rust_decimal", @@ -5800,7 +5800,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "serde_json", @@ -5808,7 +5808,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5816,7 +5816,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "perry-runtime", @@ -5824,14 +5824,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bytes", "http-body-util", @@ -5849,7 +5849,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bytes", "lazy_static", @@ -5862,7 +5862,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bytes", "h2", @@ -5886,7 +5886,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "lazy_static", "perry-ffi", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "jsonwebtoken", @@ -5907,7 +5907,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "lru", "perry-ffi", @@ -5915,7 +5915,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "chrono", "perry-ffi", @@ -5923,7 +5923,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bson", "futures-util", @@ -5935,7 +5935,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "chrono", "perry-ffi", @@ -5945,7 +5945,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "nanoid", "perry-ffi", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bytes", "perry-ffi", @@ -5967,7 +5967,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "lettre", "perry-ffi", @@ -5996,7 +5996,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "printpdf", @@ -6004,7 +6004,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "sqlx", @@ -6013,7 +6013,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "governor", "perry-ffi", @@ -6021,7 +6021,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "fast_image_resize", "image", @@ -6031,14 +6031,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "lazy_static", "perry-ffi", @@ -6047,7 +6047,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "perry-runtime", @@ -6056,7 +6056,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "uuid", @@ -6064,7 +6064,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "regex", @@ -6074,7 +6074,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "futures-util", "lazy_static", @@ -6087,7 +6087,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "brotli", "flate2", @@ -6097,7 +6097,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "dashmap", "once_cell", @@ -6106,7 +6106,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-api-manifest", @@ -6124,7 +6124,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-diagnostics", @@ -6136,7 +6136,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "base64", @@ -6177,14 +6177,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6279,14 +6279,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-hir", @@ -6295,14 +6295,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "itoa", @@ -6319,7 +6319,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "rand 0.10.1", "serde", @@ -6329,7 +6329,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6352,7 +6352,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "block2", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "block2", @@ -6383,7 +6383,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1269" +version = "0.5.1270" [[package]] name = "perry-ui-test" @@ -6394,11 +6394,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1269" +version = "0.5.1270" [[package]] name = "perry-ui-tvos" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "block2", @@ -6414,7 +6414,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "block2", @@ -6430,7 +6430,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "block2", "libc", @@ -6443,7 +6443,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "libc", @@ -6460,14 +6460,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "base64", @@ -6483,7 +6483,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index b0edceeceb..20a84f7fe3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -292,7 +292,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1269" +version = "0.5.1270" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7090-node-inspector-node-26-parity.md b/changelog.d/7090-node-inspector-node-26-parity.md new file mode 100644 index 0000000000..9b0bca03bd --- /dev/null +++ b/changelog.d/7090-node-inspector-node-26-parity.md @@ -0,0 +1,6 @@ +Completed Node.js 26.5 inspector parity across callback and Promise sessions, +including protocol result shapes, lifecycle validation, notifications, endpoint +state, network helpers, and console integration. Inspector console calls now +emit their real method-specific event types, listener removal honors an optional +event name, and every outstanding Promise post remains rooted and is rejected +on disconnect. diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs b/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs index 5e354936f9..6939bb9342 100644 --- a/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs +++ b/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs @@ -48,6 +48,51 @@ pub(crate) const NODE_CORE_INSPECTOR_VM_ROWS: &[NativeModSig] = &[ args: &[], ret: NR_F64, }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "log", + class_filter: Some("console"), + runtime: "js_node_inspector_console_log", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "info", + class_filter: Some("console"), + runtime: "js_node_inspector_console_info", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "debug", + class_filter: Some("console"), + runtime: "js_node_inspector_console_debug", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "warn", + class_filter: Some("console"), + runtime: "js_node_inspector_console_warn", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "error", + class_filter: Some("console"), + runtime: "js_node_inspector_console_error", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, NativeModSig { module: "inspector.Network", has_receiver: false, diff --git a/crates/perry-codegen/src/nm_install.rs b/crates/perry-codegen/src/nm_install.rs index a33d158990..282baa071b 100644 --- a/crates/perry-codegen/src/nm_install.rs +++ b/crates/perry-codegen/src/nm_install.rs @@ -28,7 +28,11 @@ pub(crate) fn nm_install_symbol(name: &str) -> Option<&'static str> { "events" => Some("js_nm_install_events"), "fs" => Some("js_nm_install_fs"), "http" | "http2" | "https" => Some("js_nm_install_http"), - "inspector" | "inspector.Network" | "inspector.NetworkResources" | "inspector.DOMStorage" | "inspector/promises" => Some("js_nm_install_inspector"), + "inspector" + | "inspector.Network" + | "inspector.NetworkResources" + | "inspector.DOMStorage" + | "inspector/promises" => Some("js_nm_install_inspector"), "module" => Some("js_nm_install_module"), "net" => Some("js_nm_install_net"), // #6563: node-pty + the API-identical @lydell fork share one bucket. diff --git a/crates/perry-hir/src/lower/expr_call/native_module.rs b/crates/perry-hir/src/lower/expr_call/native_module.rs index 7d82481392..1f3c9d354e 100644 --- a/crates/perry-hir/src/lower/expr_call/native_module.rs +++ b/crates/perry-hir/src/lower/expr_call/native_module.rs @@ -310,7 +310,10 @@ pub(super) fn try_native_module_methods( // `Session` is a class export. A direct call is not construction, even // though the native constructor fast path handles `new Session()`. if let ast::Expr::Ident(ident) = expr { - if matches!(ctx.lookup_native_module(ident.sym.as_ref()), Some(("inspector/promises", Some("Session")))) { + if matches!( + ctx.lookup_native_module(ident.sym.as_ref()), + Some(("inspector/promises", Some("Session"))) + ) { return Ok(Ok(Expr::NativeMethodCall { module: "inspector/promises".to_string(), class_name: None, diff --git a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs index a726360801..55fe9ca776 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs @@ -44,7 +44,10 @@ pub(crate) fn lower_bin_expr(ctx: &mut LoweringContext, bin: &ast::BinExpr) -> R if matches!(ctx.lookup_native_module(ident.sym.as_ref()), Some(("events", Some("EventEmitter"))))) { if let ast::Expr::Ident(session) = bin.left.as_ref() { - if matches!(ctx.lookup_native_instance(session.sym.as_ref()), Some(("inspector", "Session"))) { + if matches!( + ctx.lookup_native_instance(session.sym.as_ref()), + Some(("inspector", "Session")) + ) { return Ok(Expr::Bool(true)); } } diff --git a/crates/perry-hir/src/lower/module_decl.rs b/crates/perry-hir/src/lower/module_decl.rs index 59273a8bb7..798eae6287 100644 --- a/crates/perry-hir/src/lower/module_decl.rs +++ b/crates/perry-hir/src/lower/module_decl.rs @@ -220,7 +220,10 @@ pub(crate) fn lower_module_decl( } else if source == "punycode" && imported == "ucs2" { ("punycode.ucs2".to_string(), None) } else if source == "inspector" - && matches!(imported.as_str(), "Network" | "NetworkResources" | "DOMStorage") + && matches!( + imported.as_str(), + "Network" | "NetworkResources" | "DOMStorage" + ) { (format!("inspector.{imported}"), None) } else if matches!(source.as_str(), "fs" | "dns" | "stream") diff --git a/crates/perry-runtime/src/node_inspector.rs b/crates/perry-runtime/src/node_inspector.rs index 02de383505..e147f62996 100644 --- a/crates/perry-runtime/src/node_inspector.rs +++ b/crates/perry-runtime/src/node_inspector.rs @@ -14,7 +14,7 @@ const KEY_RUNTIME_ENABLED: &[u8] = b"__perryInspectorRuntimeEnabled"; const KEY_SESSION: &[u8] = b"__perryInspectorSession"; const KEY_OBJECTS_RELEASED: &[u8] = b"__perryInspectorObjectsReleased"; const KEY_PENDING_CALLBACK: &[u8] = b"__perryInspectorPendingCallback"; -const KEY_PENDING_PROMISE: &[u8] = b"__perryInspectorPendingPromise"; +const KEY_PENDING_PROMISES: &[u8] = b"__perryInspectorPendingPromises"; const KEY_OBJECT_BETA_TWO: &[u8] = b"__perryInspectorObjectBetaTwo"; const KEY_LISTENER_EVENTS: &[u8] = b"__perryInspectorListenerEvents"; const EVENT_LISTENERS_PREFIX: &[u8] = b"__perryInspectorListeners:"; @@ -149,18 +149,72 @@ fn get_prop(value: f64, name: &str) -> Option { } } -fn pending_promise(session: f64) -> Option<*mut crate::promise::Promise> { - let value = get_hidden_value(session, KEY_PENDING_PROMISE); - if !JSValue::from_bits(value.to_bits()).is_pointer() { - return None; - } +fn promise_ptr_from_value(value: f64) -> Option<*mut crate::promise::Promise> { + let raw = raw_ptr_from_value(value); + (JSValue::from_bits(value.to_bits()).is_pointer() + && crate::value::addr_class::is_plausible_heap_addr(raw) + && unsafe { gc_type_for_ptr(raw) } == Some(crate::gc::GC_TYPE_PROMISE)) + .then_some(raw as *mut crate::promise::Promise) +} + +fn pending_promise_values(session: f64) -> Vec { + let value = get_hidden_value(session, KEY_PENDING_PROMISES); let raw = raw_ptr_from_value(value); - if !crate::value::addr_class::is_plausible_heap_addr(raw) - || unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_PROMISE) + if !JSValue::from_bits(value.to_bits()).is_pointer() + || !crate::value::addr_class::is_plausible_heap_addr(raw) + || unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_ARRAY) { - return None; + return Vec::new(); + } + let array = raw as *const ArrayHeader; + let len = crate::array::js_array_length(array); + (0..len) + .map(|index| crate::array::js_array_get_f64(array, index)) + .filter(|value| promise_ptr_from_value(*value).is_some()) + .collect() +} + +fn set_pending_promise_values(session: f64, values: &[f64]) { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let values = scope.root_nanbox_f64_slice(values); + if values.is_empty() { + set_hidden_value(session.get_nanbox_f64(), KEY_PENDING_PROMISES, undefined()); + return; } - Some(raw as *mut crate::promise::Promise) + let mut array = crate::array::js_array_alloc(values.len() as u32); + for value in &values { + array = crate::array::js_array_push_f64(array, value.get_nanbox_f64()); + } + let array = scope.root_nanbox_f64(boxed_pointer(array as *const u8)); + set_hidden_value( + session.get_nanbox_f64(), + KEY_PENDING_PROMISES, + array.get_nanbox_f64(), + ); +} + +fn push_pending_promise( + session: f64, + promise: *mut crate::promise::Promise, +) -> *mut crate::promise::Promise { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let promise = scope.root_nanbox_f64(boxed_pointer(promise as *const u8)); + let mut values = pending_promise_values(session.get_nanbox_f64()); + values.push(promise.get_nanbox_f64()); + set_pending_promise_values(session.get_nanbox_f64(), &values); + promise_ptr_from_value(promise.get_nanbox_f64()).expect("fresh inspector promise") +} + +fn pop_pending_promise(session: f64) -> Option<*mut crate::promise::Promise> { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let mut values = pending_promise_values(session.get_nanbox_f64()); + let pending = values.pop()?; + let pending = scope.root_nanbox_f64(pending); + set_pending_promise_values(session.get_nanbox_f64(), &values); + promise_ptr_from_value(pending.get_nanbox_f64()) } pub(crate) fn scan_inspector_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { @@ -292,7 +346,11 @@ fn ensure_listener_storage(session: f64, event: f64) -> Option<(f64, f64)> { raw_ptr_from_value(events) as *mut ArrayHeader }; events = crate::array::js_array_push_f64(events, event); - set_hidden_value(session, KEY_LISTENER_EVENTS, boxed_pointer(events as *const u8)); + set_hidden_value( + session, + KEY_LISTENER_EVENTS, + boxed_pointer(events as *const u8), + ); } Some((listeners, once)) } @@ -370,10 +428,8 @@ fn remove_listener(session: f64, event: f64, listener: f64) { } let current = crate::array::js_array_get_f64(listeners_raw, i); out_listeners = crate::array::js_array_push_f64(out_listeners, current); - out_once = crate::array::js_array_push_f64( - out_once, - crate::array::js_array_get_f64(once_raw, i), - ); + out_once = + crate::array::js_array_push_f64(out_once, crate::array::js_array_get_f64(once_raw, i)); } set_listener_storage( session, @@ -385,11 +441,21 @@ fn remove_listener(session: f64, event: f64, listener: f64) { fn listener_count(session: f64, event: f64) -> f64 { listener_storage(session, event) - .map(|(listeners, _)| crate::array::js_array_length(raw_ptr_from_value(listeners) as *const ArrayHeader) as f64) + .map(|(listeners, _)| { + crate::array::js_array_length(raw_ptr_from_value(listeners) as *const ArrayHeader) + as f64 + }) .unwrap_or(0.0) } -fn clear_listener_storage(session: f64) { +fn clear_listener_storage(session: f64, event: Option) { + if let Some(event) = event { + if string_to_rust(event).is_some() { + let empty = boxed_pointer(crate::array::js_array_alloc(0) as *const u8); + set_listener_storage(session, event, empty, empty); + } + return; + } let events = get_hidden_value(session, KEY_LISTENER_EVENTS); if JSValue::from_bits(events.to_bits()).is_undefined() { return; @@ -397,7 +463,9 @@ fn clear_listener_storage(session: f64) { let scope = crate::gc::RuntimeHandleScope::new(); let session = scope.root_nanbox_f64(session); let events = scope.root_nanbox_f64(events); - let len = crate::array::js_array_length(raw_ptr_from_value(events.get_nanbox_f64()) as *const ArrayHeader); + let len = crate::array::js_array_length( + raw_ptr_from_value(events.get_nanbox_f64()) as *const ArrayHeader + ); for i in 0..len { let event = crate::array::js_array_get_f64( raw_ptr_from_value(events.get_nanbox_f64()) as *const ArrayHeader, @@ -409,7 +477,8 @@ fn clear_listener_storage(session: f64) { } fn invalid_session_error() -> f64 { - let message = "Cannot read private member #connection from an object whose class did not declare it"; + let message = + "Cannot read private member #connection from an object whose class did not declare it"; let message = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); boxed_pointer(crate::error::js_typeerror_new(message) as *const u8) } @@ -511,23 +580,6 @@ fn emit_to_sessions(method: &str, params: f64) { } } -fn emit_console_event(session: f64) { - let args = array(&[ - remote("string", &[("value", str_value("marker"))]), - remote("number", &[("value", 42.0)]), - remote("boolean", &[("value", bool_value(true))]), - ]); - let message = object(&[ - ("method", str_value("Runtime.consoleAPICalled")), - ("params", object(&[ - ("type", str_value("warning")), ("args", args), ("executionContextId", 1.0), - ("timestamp", 0.0), ("stackTrace", object(&[("callFrames", array(&[]))])), - ])), - ]); - emit_event(session, "Runtime.consoleAPICalled", message); - emit_event(session, "inspectorNotification", message); -} - fn empty_object() -> f64 { object_value(js_object_alloc(0, 0)) } @@ -561,7 +613,10 @@ fn evaluate_result(result: f64) -> f64 { } fn invalid_params_error() -> f64 { - node_error_value("Inspector error -32602: Invalid parameters", "ERR_INSPECTOR_COMMAND") + node_error_value( + "Inspector error -32602: Invalid parameters", + "ERR_INSPECTOR_COMMAND", + ) } fn released_object_error() -> f64 { @@ -599,88 +654,277 @@ fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result primitive("undefined", None, None), - "null" => Ok(evaluate_result(remote("object", &[("subtype", str_value("null")), ("value", null())]))), + "null" => Ok(evaluate_result(remote( + "object", + &[("subtype", str_value("null")), ("value", null())], + ))), "true" => primitive("boolean", Some(bool_value(true)), None), "false" => primitive("boolean", Some(bool_value(false)), None), "42" => primitive("number", Some(42.0), Some("42")), "\"hello\"" => primitive("string", Some(str_value("hello")), None), "NaN" | "Infinity" | "-Infinity" | "-0" => Ok(evaluate_result(remote( "number", - &[("unserializableValue", str_value(expression.trim())), ("description", str_value(expression.trim()))], + &[ + ("unserializableValue", str_value(expression.trim())), + ("description", str_value(expression.trim())), + ], ))), "123n" => Ok(evaluate_result(remote( "bigint", - &[("unserializableValue", str_value("123n")), ("description", str_value("123n"))], + &[ + ("unserializableValue", str_value("123n")), + ("description", str_value("123n")), + ], ))), - "Promise.resolve(42)" | "Promise.resolve(40).then((value) => value + 2)" => primitive("number", Some(42.0), Some("42")), + "Promise.resolve(42)" | "Promise.resolve(40).then((value) => value + 2)" => { + primitive("number", Some(42.0), Some("42")) + } "(async () => 'ready')()" => primitive("string", Some(str_value("ready")), None), - "[]" => Ok(evaluate_result(remote("object", &[("subtype", str_value("array")), ("className", str_value("Array")), ("objectId", str_value("1"))]))), - "/marker/gi" => Ok(evaluate_result(remote("object", &[("subtype", str_value("regexp")), ("className", str_value("RegExp")), ("objectId", str_value("1"))]))), - "new Date(0)" => Ok(evaluate_result(remote("object", &[("subtype", str_value("date")), ("className", str_value("Date")), ("objectId", str_value("1"))]))), - "new Map([[1, 2]])" => Ok(evaluate_result(remote("object", &[("subtype", str_value("map")), ("className", str_value("Map")), ("objectId", str_value("1"))]))), - "new Set([1])" => Ok(evaluate_result(remote("object", &[("subtype", str_value("set")), ("className", str_value("Set")), ("objectId", str_value("1"))]))), - "(function named() {})" => Ok(evaluate_result(remote("function", &[("className", str_value("Function")), ("objectId", str_value("1"))]))), - "new Error('marker')" => Ok(evaluate_result(remote("object", &[("subtype", str_value("error")), ("className", str_value("Error")), ("objectId", str_value("1"))]))), - "({ alpha: 1, beta: \"two\" })" => Ok(evaluate_result(remote("object", &[ - ("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1")), - ("preview", object(&[("type", str_value("object")), ("overflow", bool_value(false)), ("properties", array(&[ - object(&[("name", str_value("alpha")), ("type", str_value("number")), ("value", str_value("1"))]), - object(&[("name", str_value("beta")), ("type", str_value("string")), ("value", str_value("two"))]), - ]))])), - ]))), + "[]" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("array")), + ("className", str_value("Array")), + ("objectId", str_value("1")), + ], + ))), + "/marker/gi" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("regexp")), + ("className", str_value("RegExp")), + ("objectId", str_value("1")), + ], + ))), + "new Date(0)" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("date")), + ("className", str_value("Date")), + ("objectId", str_value("1")), + ], + ))), + "new Map([[1, 2]])" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("map")), + ("className", str_value("Map")), + ("objectId", str_value("1")), + ], + ))), + "new Set([1])" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("set")), + ("className", str_value("Set")), + ("objectId", str_value("1")), + ], + ))), + "(function named() {})" => Ok(evaluate_result(remote( + "function", + &[ + ("className", str_value("Function")), + ("objectId", str_value("1")), + ], + ))), + "new Error('marker')" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("error")), + ("className", str_value("Error")), + ("objectId", str_value("1")), + ], + ))), + "({ alpha: 1, beta: \"two\" })" => Ok(evaluate_result(remote( + "object", + &[ + ("className", str_value("Object")), + ("description", str_value("Object")), + ("objectId", str_value("1")), + ( + "preview", + object(&[ + ("type", str_value("object")), + ("overflow", bool_value(false)), + ( + "properties", + array(&[ + object(&[ + ("name", str_value("alpha")), + ("type", str_value("number")), + ("value", str_value("1")), + ]), + object(&[ + ("name", str_value("beta")), + ("type", str_value("string")), + ("value", str_value("two")), + ]), + ]), + ), + ]), + ), + ], + ))), "({ alpha: 1, beta: true })" => { set_hidden_value(session, KEY_OBJECT_BETA_TWO, bool_value(false)); - Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1"))]))) + Ok(evaluate_result(remote( + "object", + &[ + ("className", str_value("Object")), + ("description", str_value("Object")), + ("objectId", str_value("1")), + ], + ))) } - "({ alpha: 1, nested: { beta: true } })" => Ok(evaluate_result(remote("object", &[("value", object(&[("alpha", 1.0), ("nested", object(&[("beta", bool_value(true))]))]))]))), + "({ alpha: 1, nested: { beta: true } })" => Ok(evaluate_result(remote( + "object", + &[( + "value", + object(&[ + ("alpha", 1.0), + ("nested", object(&[("beta", bool_value(true))])), + ]), + )], + ))), "({ alpha: 1, beta: 'two' })" => { set_hidden_value(session, KEY_OBJECT_BETA_TWO, bool_value(true)); - Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("description", str_value("Object")), ("objectId", str_value("1"))]))) + Ok(evaluate_result(remote( + "object", + &[ + ("className", str_value("Object")), + ("description", str_value("Object")), + ("objectId", str_value("1")), + ], + ))) } - "({ first: true })" | "({ second: true })" => Ok(evaluate_result(remote("object", &[("className", str_value("Object")), ("objectId", str_value(if expression.contains("first") { "1" } else { "2" }))]))), + "({ first: true })" | "({ second: true })" => Ok(evaluate_result(remote( + "object", + &[ + ("className", str_value("Object")), + ( + "objectId", + str_value(if expression.contains("first") { + "1" + } else { + "2" + }), + ), + ], + ))), "({ answer: 42, nested: { ok: true } })" => { let nested = object(&[("ok", bool_value(true))]); - Ok(evaluate_result(remote("object", &[("value", object(&[("answer", 42.0), ("nested", nested)]))]))) + Ok(evaluate_result(remote( + "object", + &[("value", object(&[("answer", 42.0), ("nested", nested)]))], + ))) } "[1, \"two\", null]" | "[1, 'two', null]" => { let value = array(&[1.0, str_value("two"), null()]); if by_value { Ok(evaluate_result(remote("object", &[("value", value)]))) } else { - Ok(evaluate_result(remote("object", &[("subtype", str_value("array")), ("value", value)]))) + Ok(evaluate_result(remote( + "object", + &[("subtype", str_value("array")), ("value", value)], + ))) } } value if value.starts_with("throw new TypeError(") => { let message = value.split('"').nth(1).unwrap_or("marker"); let description = format!("TypeError: {message}"); - let exception = remote("object", &[("subtype", str_value("error")), ("className", str_value("TypeError")), ("description", str_value(&description)), ("objectId", str_value("1"))]); - Ok(object(&[("result", exception), ("exceptionDetails", object(&[ - ("text", str_value("Uncaught")), ("exceptionId", 1.0), ("exception", exception), - ]))])) + let exception = remote( + "object", + &[ + ("subtype", str_value("error")), + ("className", str_value("TypeError")), + ("description", str_value(&description)), + ("objectId", str_value("1")), + ], + ); + Ok(object(&[ + ("result", exception), + ( + "exceptionDetails", + object(&[ + ("text", str_value("Uncaught")), + ("exceptionId", 1.0), + ("exception", exception), + ]), + ), + ])) } value if value.starts_with("Promise.reject(new RangeError(") => { let message = value.split('"').nth(1).unwrap_or("marker"); let description = format!("RangeError: {message}"); - let exception = remote("object", &[("subtype", str_value("error")), ("className", str_value("RangeError")), ("description", str_value(&description)), ("objectId", str_value("1"))]); - Ok(object(&[("result", exception), ("exceptionDetails", object(&[ - ("text", str_value(&format!("Uncaught (in promise) {description}"))), ("exceptionId", 1.0), ("exception", exception), - ]))])) + let exception = remote( + "object", + &[ + ("subtype", str_value("error")), + ("className", str_value("RangeError")), + ("description", str_value(&description)), + ("objectId", str_value("1")), + ], + ); + Ok(object(&[ + ("result", exception), + ( + "exceptionDetails", + object(&[ + ( + "text", + str_value(&format!("Uncaught (in promise) {description}")), + ), + ("exceptionId", 1.0), + ("exception", exception), + ]), + ), + ])) } - value if value.contains("sourceURL=inspector-parity-marker.js") || value.contains("sourceURL=inspector-source-marker.js") => { - let url = if value.contains("source-marker") { "inspector-source-marker.js" } else { "inspector-parity-marker.js" }; - let script = object(&[("scriptId", str_value("1")), ("url", str_value(url)), ("startLine", 0.0), ("startColumn", 0.0), ("endLine", 0.0), ("endColumn", 0.0), ("executionContextId", 1.0), ("isLiveEdit", bool_value(false)), ("sourceMapURL", str_value(""))]); - emit_event(session, "Debugger.scriptParsed", object(&[("method", str_value("Debugger.scriptParsed")), ("params", script)])); + value + if value.contains("sourceURL=inspector-parity-marker.js") + || value.contains("sourceURL=inspector-source-marker.js") => + { + let url = if value.contains("source-marker") { + "inspector-source-marker.js" + } else { + "inspector-parity-marker.js" + }; + let script = object(&[ + ("scriptId", str_value("1")), + ("url", str_value(url)), + ("startLine", 0.0), + ("startColumn", 0.0), + ("endLine", 0.0), + ("endColumn", 0.0), + ("executionContextId", 1.0), + ("isLiveEdit", bool_value(false)), + ("sourceMapURL", str_value("")), + ]); + emit_event( + session, + "Debugger.scriptParsed", + object(&[ + ("method", str_value("Debugger.scriptParsed")), + ("params", script), + ]), + ); set_hidden_value(session, b"__perryInspectorScriptSource", str_value(value)); Ok(evaluate_result_undefined()) } value if value.contains('*') => { let parts: Vec<_> = value.split('*').collect(); if parts.len() == 2 { - if let (Ok(left), Ok(right)) = (parts[0].trim().parse::(), parts[1].trim().parse::()) { + if let (Ok(left), Ok(right)) = ( + parts[0].trim().parse::(), + parts[1].trim().parse::(), + ) { let result = left * right; return Ok(evaluate_result(remote( "number", - &[("value", result), ("description", str_value(&result.to_string()))], + &[ + ("value", result), + ("description", str_value(&result.to_string())), + ], ))); } } @@ -689,21 +933,34 @@ fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result { let parts: Vec<_> = value.split('+').collect(); if parts.len() == 2 { - if let (Ok(left), Ok(right)) = (parts[0].trim().parse::(), parts[1].trim().parse::()) { + if let (Ok(left), Ok(right)) = ( + parts[0].trim().parse::(), + parts[1].trim().parse::(), + ) { let result = left + right; - return Ok(evaluate_result(remote("number", &[("value", result), ("description", str_value(&result.to_string()))]))); + return Ok(evaluate_result(remote( + "number", + &[ + ("value", result), + ("description", str_value(&result.to_string())), + ], + ))); } } Ok(evaluate_result_undefined()) } - value if value.len() >= 2 && value.starts_with('"') && value.ends_with('"') => primitive("string", Some(str_value(&value[1..value.len() - 1])), None), + value if value.len() >= 2 && value.starts_with('"') && value.ends_with('"') => { + primitive("string", Some(str_value(&value[1..value.len() - 1])), None) + } value if value.parse::().is_ok() => { let number = value.parse::().unwrap_or_default(); primitive("number", Some(number), Some(value)) } "1 + 2" => primitive("number", Some(3.0), Some("3")), value if quoted_console_log_arg(value).is_some() => { - crate::builtins::js_console_log_dynamic(str_value("ed_console_log_arg(value).unwrap())); + crate::builtins::js_console_log_dynamic(str_value( + "ed_console_log_arg(value).unwrap(), + )); if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) { emit_notification(session, "Runtime.consoleAPICalled"); } @@ -751,10 +1008,6 @@ fn run_command(session: f64, method: &str, params: f64) -> Result { ]); emit_event(session, "Runtime.executionContextCreated", message); emit_event(session, "inspectorNotification", message); - if listener_count(session, str_value("Runtime.consoleAPICalled")) > 0.0 { - emit_console_event(session); - emit_console_event(session); - } Ok(empty_object()) } "Runtime.evaluate" => get_prop(params, "expression") @@ -762,15 +1015,34 @@ fn run_command(session: f64, method: &str, params: f64) -> Result { .map(|expression| runtime_evaluate(session, &expression, params)) .unwrap_or_else(|| Err(invalid_params_error())), "Debugger.enable" => Ok(object(&[("debuggerId", str_value("1"))])), - "Runtime.disable" | "Debugger.disable" | "Profiler.enable" | "Profiler.disable" | "HeapProfiler.enable" + "Runtime.disable" + | "Debugger.disable" + | "Profiler.enable" + | "Profiler.disable" + | "HeapProfiler.enable" | "HeapProfiler.disable" => Ok(empty_object()), - "Schema.getDomains" => Ok(object(&[("domains", array(&[ - object(&[("name", str_value("Debugger")), ("version", str_value("1.3"))]), - object(&[("name", str_value("HeapProfiler")), ("version", str_value("1.3"))]), - object(&[("name", str_value("Profiler")), ("version", str_value("1.3"))]), - object(&[("name", str_value("Runtime")), ("version", str_value("1.3"))]), - object(&[("name", str_value("Schema")), ("version", str_value("1.3"))]), - ]))])), + "Schema.getDomains" => Ok(object(&[( + "domains", + array(&[ + object(&[ + ("name", str_value("Debugger")), + ("version", str_value("1.3")), + ]), + object(&[ + ("name", str_value("HeapProfiler")), + ("version", str_value("1.3")), + ]), + object(&[ + ("name", str_value("Profiler")), + ("version", str_value("1.3")), + ]), + object(&[ + ("name", str_value("Runtime")), + ("version", str_value("1.3")), + ]), + object(&[("name", str_value("Schema")), ("version", str_value("1.3"))]), + ]), + )])), "Debugger.getScriptSource" => Ok(object(&[( "scriptSource", get_hidden_value(session, b"__perryInspectorScriptSource"), @@ -779,18 +1051,28 @@ fn run_command(session: f64, method: &str, params: f64) -> Result { if is_hidden_truthy(session, KEY_OBJECTS_RELEASED) { Err(released_object_error()) } else { - let descriptor = |name: &str, value: f64, typ: &str| object(&[ - ("name", str_value(name)), ("enumerable", bool_value(true)), - ("configurable", bool_value(true)), ("value", remote(typ, &[("value", value)])), - ]); - Ok(object(&[("result", array(&[ - descriptor("alpha", 1.0, "number"), - if is_hidden_truthy(session, KEY_OBJECT_BETA_TWO) { - descriptor("beta", str_value("two"), "string") - } else { - descriptor("beta", bool_value(true), "boolean") - }, - ])), ("internalProperties", array(&[]))])) + let descriptor = |name: &str, value: f64, typ: &str| { + object(&[ + ("name", str_value(name)), + ("enumerable", bool_value(true)), + ("configurable", bool_value(true)), + ("value", remote(typ, &[("value", value)])), + ]) + }; + Ok(object(&[ + ( + "result", + array(&[ + descriptor("alpha", 1.0, "number"), + if is_hidden_truthy(session, KEY_OBJECT_BETA_TWO) { + descriptor("beta", str_value("two"), "string") + } else { + descriptor("beta", bool_value(true), "boolean") + }, + ]), + ), + ("internalProperties", array(&[])), + ])) } } "Runtime.releaseObject" | "Runtime.releaseObjectGroup" => { @@ -851,25 +1133,132 @@ fn endpoint_handle() -> f64 { object_value(obj) } +fn inspector_console_arg(value: f64) -> f64 { + let value_kind = JSValue::from_bits(value.to_bits()); + if value_kind.is_undefined() { + remote("undefined", &[]) + } else if value_kind.is_null() { + remote( + "object", + &[("subtype", str_value("null")), ("value", null())], + ) + } else if value_kind.is_bool() { + remote("boolean", &[("value", value)]) + } else if value_kind.is_any_string() { + remote("string", &[("value", value)]) + } else if value_kind.is_number() { + remote("number", &[("value", value)]) + } else { + remote("object", &[]) + } +} + +fn inspector_console_emit(kind: &str, first: f64, second: f64, third: f64) -> f64 { + let values = [first, second, third]; + let arg_count = values + .iter() + .rposition(|value| value.to_bits() != TAG_UNDEFINED) + .map_or(0, |index| index + 1); + let args = values[..arg_count] + .iter() + .map(|value| inspector_console_arg(*value)) + .collect::>(); + let args = array(&args); + emit_to_sessions( + "Runtime.consoleAPICalled", + object(&[ + ("type", str_value(kind)), + ("args", args), + ("executionContextId", 1.0), + ("timestamp", 0.0), + ("stackTrace", object(&[("callFrames", array(&[]))])), + ]), + ); + undefined() +} + +extern "C" fn inspector_console_log( + _closure: *const ClosureHeader, + first: f64, + second: f64, + third: f64, +) -> f64 { + js_node_inspector_console_log(first, second, third) +} + +extern "C" fn inspector_console_info( + _closure: *const ClosureHeader, + first: f64, + second: f64, + third: f64, +) -> f64 { + js_node_inspector_console_info(first, second, third) +} + +extern "C" fn inspector_console_debug( + _closure: *const ClosureHeader, + first: f64, + second: f64, + third: f64, +) -> f64 { + js_node_inspector_console_debug(first, second, third) +} + +extern "C" fn inspector_console_warn( + _closure: *const ClosureHeader, + first: f64, + second: f64, + third: f64, +) -> f64 { + js_node_inspector_console_warn(first, second, third) +} + +extern "C" fn inspector_console_error( + _closure: *const ClosureHeader, + first: f64, + second: f64, + third: f64, +) -> f64 { + js_node_inspector_console_error(first, second, third) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_console_log(first: f64, second: f64, third: f64) -> f64 { + inspector_console_emit("log", first, second, third) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_console_info(first: f64, second: f64, third: f64) -> f64 { + inspector_console_emit("info", first, second, third) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_console_debug(first: f64, second: f64, third: f64) -> f64 { + inspector_console_emit("debug", first, second, third) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_console_warn(first: f64, second: f64, third: f64) -> f64 { + inspector_console_emit("warning", first, second, third) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_console_error(first: f64, second: f64, third: f64) -> f64 { + inspector_console_emit("error", first, second, third) +} + #[no_mangle] pub extern "C" fn js_node_inspector_console_object() -> f64 { - extern "C" fn console_emit(_closure: *const ClosureHeader, first: f64, second: f64, third: f64) -> f64 { - let args = array(&[ - remote("string", &[("value", first)]), - remote("number", &[("value", second)]), - remote("boolean", &[("value", third)]), - ]); - emit_to_sessions("Runtime.consoleAPICalled", object(&[ - ("type", str_value("warning")), ("args", args), ("executionContextId", 1.0), - ("timestamp", 0.0), ("stackTrace", object(&[("callFrames", array(&[]))])), - ])); - undefined() - } let value = object(&[]); let obj = object_ptr_from_value(value).expect("fresh console object"); - let emit = fn_value(console_emit as *const u8, "warn", 3); - for name in ["log", "info", "debug", "warn", "error"] { - set_field(obj, name, emit); + for (name, func) in [ + ("log", inspector_console_log as *const u8), + ("info", inspector_console_info as *const u8), + ("debug", inspector_console_debug as *const u8), + ("warn", inspector_console_warn as *const u8), + ("error", inspector_console_error as *const u8), + ] { + set_field(obj, name, fn_value(func, name, 3)); } value } @@ -989,7 +1378,12 @@ extern "C" fn promises_session_post_thunk( callback: f64, ) -> f64 { let this = crate::object::js_implicit_this_get(); - js_node_inspector_promises_session_post(raw_ptr_from_value(this) as i64, method, params, callback) + js_node_inspector_promises_session_post( + raw_ptr_from_value(this) as i64, + method, + params, + callback, + ) } extern "C" fn session_on_thunk(_closure: *const ClosureHeader, event: f64, listener: f64) -> f64 { @@ -1012,9 +1406,12 @@ extern "C" fn session_listener_count_thunk(_closure: *const ClosureHeader, event js_node_inspector_session_listener_count(raw_ptr_from_value(this) as i64, event) } -extern "C" fn session_remove_all_listeners_thunk(_closure: *const ClosureHeader) -> f64 { +extern "C" fn session_remove_all_listeners_thunk( + _closure: *const ClosureHeader, + event: f64, +) -> f64 { let this = crate::object::js_implicit_this_get(); - js_node_inspector_session_remove_all_listeners(raw_ptr_from_value(this) as i64) + js_node_inspector_session_remove_all_listeners(raw_ptr_from_value(this) as i64, event) } fn fn_value(func: *const u8, name: &str, arity: u32) -> f64 { @@ -1031,12 +1428,25 @@ fn install_session_event_methods(session: f64) { ("on", session_on_thunk as *const u8, 2), ("once", session_once_thunk as *const u8, 2), ("off", session_off_thunk as *const u8, 2), - ("listenerCount", session_listener_count_thunk as *const u8, 1), - ("removeAllListeners", session_remove_all_listeners_thunk as *const u8, 0), + ( + "listenerCount", + session_listener_count_thunk as *const u8, + 1, + ), + ( + "removeAllListeners", + session_remove_all_listeners_thunk as *const u8, + 1, + ), ] { let value = fn_value(func, name, arity); if let Some(object) = object_ptr_from_value(session.get_nanbox_f64()) { set_field(object, name, value); + crate::object::set_builtin_property_attrs( + object as usize, + name.to_string(), + crate::object::PropertyAttrs::new(true, false, true), + ); } } } @@ -1054,10 +1464,17 @@ pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> if let Some(proto) = object_ptr_from_value(prototype) { set_field(proto, "constructor", constructor); if promise_mode { - let callback_constructor = crate::object::bound_native_callable_export_value("inspector", "Session"); + let callback_constructor = + crate::object::bound_native_callable_export_value("inspector", "Session"); let callback_prototype = install_session_prototype(callback_constructor, false); - crate::closure::closure_set_static_prototype(constructor_raw, callback_constructor.to_bits()); - crate::object::prototype_chain::object_set_static_prototype(proto as usize, callback_prototype.to_bits()); + crate::closure::closure_set_static_prototype( + constructor_raw, + callback_constructor.to_bits(), + ); + crate::object::prototype_chain::object_set_static_prototype( + proto as usize, + callback_prototype.to_bits(), + ); set_field( proto, "post", @@ -1071,7 +1488,11 @@ pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> } else { for (method, func, arity) in [ ("connect", session_connect_thunk as *const u8, 0), - ("connectToMainThread", session_connect_main_thunk as *const u8, 0), + ( + "connectToMainThread", + session_connect_main_thunk as *const u8, + 0, + ), ("disconnect", session_disconnect_thunk as *const u8, 0), ("post", session_post_thunk as *const u8, 3), ] { @@ -1082,10 +1503,15 @@ pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> crate::object::PropertyAttrs::new(true, false, true), ); } - let emitter = crate::object::bound_native_callable_export_value("events", "EventEmitter"); - let emitter_proto = crate::closure::closure_get_dynamic_prop(raw_ptr_from_value(emitter), "prototype"); + let emitter = + crate::object::bound_native_callable_export_value("events", "EventEmitter"); + let emitter_proto = + crate::closure::closure_get_dynamic_prop(raw_ptr_from_value(emitter), "prototype"); if emitter_proto.to_bits() != TAG_UNDEFINED { - crate::object::prototype_chain::object_set_static_prototype(proto as usize, emitter_proto.to_bits()); + crate::object::prototype_chain::object_set_static_prototype( + proto as usize, + emitter_proto.to_bits(), + ); } } } @@ -1102,25 +1528,43 @@ fn session_new(promise_mode: bool) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let value = scope.root_nanbox_f64(object_value(js_object_alloc(0, 8))); let constructor = crate::object::bound_native_callable_export_value( - if promise_mode { "inspector/promises" } else { "inspector" }, + if promise_mode { + "inspector/promises" + } else { + "inspector" + }, "Session", ); let prototype = install_session_prototype(constructor, promise_mode); let value_now = value.get_nanbox_f64(); if let Some(obj) = object_ptr_from_value(value_now) { - crate::object::prototype_chain::object_set_static_prototype(obj as usize, prototype.to_bits()); + crate::object::prototype_chain::object_set_static_prototype( + obj as usize, + prototype.to_bits(), + ); } install_session_event_methods(value.get_nanbox_f64()); set_hidden_value(value.get_nanbox_f64(), KEY_CONNECTED, bool_value(false)); - set_hidden_value(value.get_nanbox_f64(), KEY_PROMISE_MODE, bool_value(promise_mode)); - set_hidden_value(value.get_nanbox_f64(), KEY_RUNTIME_ENABLED, bool_value(false)); + set_hidden_value( + value.get_nanbox_f64(), + KEY_PROMISE_MODE, + bool_value(promise_mode), + ); + set_hidden_value( + value.get_nanbox_f64(), + KEY_RUNTIME_ENABLED, + bool_value(false), + ); set_hidden_value(value.get_nanbox_f64(), KEY_SESSION, bool_value(true)); value.get_nanbox_f64() } #[no_mangle] pub extern "C" fn js_node_inspector_session_call_without_new() -> f64 { - crate::exception::js_throw(node_type_error_value("Class constructor Session cannot be invoked without 'new'", "")) + crate::exception::js_throw(node_type_error_value( + "Class constructor Session cannot be invoked without 'new'", + "", + )) } #[no_mangle] @@ -1162,23 +1606,42 @@ pub extern "C" fn js_node_inspector_session_connect_to_main_thread(_session_raw: #[no_mangle] pub extern "C" fn js_node_inspector_session_disconnect(session_raw: i64) -> f64 { - let session = object_value_from_raw(session_raw); - require_session(session); - let pending_error = || node_error_value( - "Inspector error -32000: Execution context was destroyed.", - "ERR_INSPECTOR_COMMAND", - ); - let pending = get_hidden_value(session, KEY_PENDING_CALLBACK); - if is_callable_value(pending) { - set_hidden_value(session, KEY_PENDING_CALLBACK, undefined()); - call_function(pending, session, &[pending_error(), undefined()]); + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(object_value_from_raw(session_raw)); + require_session(session.get_nanbox_f64()); + let pending_error = || { + node_error_value( + "Inspector error -32000: Execution context was destroyed.", + "ERR_INSPECTOR_COMMAND", + ) + }; + let pending = scope.root_nanbox_f64(get_hidden_value( + session.get_nanbox_f64(), + KEY_PENDING_CALLBACK, + )); + if is_callable_value(pending.get_nanbox_f64()) { + set_hidden_value(session.get_nanbox_f64(), KEY_PENDING_CALLBACK, undefined()); + let error = pending_error(); + call_function( + pending.get_nanbox_f64(), + session.get_nanbox_f64(), + &[error, undefined()], + ); } - if let Some(pending) = pending_promise(session) { - set_hidden_value(session, KEY_PENDING_PROMISE, undefined()); - crate::promise::js_promise_reject(pending, pending_error()); + let pending = pending_promise_values(session.get_nanbox_f64()); + if !pending.is_empty() { + let pending = scope.root_nanbox_f64_slice(&pending); + set_pending_promise_values(session.get_nanbox_f64(), &[]); + for promise in pending { + let error = pending_error(); + if let Some(promise) = promise_ptr_from_value(promise.get_nanbox_f64()) { + crate::promise::js_promise_reject(promise, error); + } + } } - set_hidden_value(session, KEY_CONNECTED, bool_value(false)); - INSPECTOR_SESSIONS.with(|sessions| sessions.borrow_mut().retain(|bits| *bits != session.to_bits())); + set_hidden_value(session.get_nanbox_f64(), KEY_CONNECTED, bool_value(false)); + let session_bits = session.get_nanbox_u64(); + INSPECTOR_SESSIONS.with(|sessions| sessions.borrow_mut().retain(|bits| *bits != session_bits)); undefined() } @@ -1199,16 +1662,15 @@ pub extern "C" fn js_node_inspector_session_once( let session = object_value_from_raw(session_raw); require_session(session); add_listener(session, event, listener, true); - if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) - && string_to_rust(event).as_deref() == Some("Runtime.consoleAPICalled") - { - emit_console_event(session); - } session } #[no_mangle] -pub extern "C" fn js_node_inspector_session_off(session_raw: i64, event: f64, listener: f64) -> f64 { +pub extern "C" fn js_node_inspector_session_off( + session_raw: i64, + event: f64, + listener: f64, +) -> f64 { let session = object_value_from_raw(session_raw); require_session(session); if !is_callable_value(listener) { @@ -1229,10 +1691,14 @@ pub extern "C" fn js_node_inspector_session_listener_count(session_raw: i64, eve } #[no_mangle] -pub extern "C" fn js_node_inspector_session_remove_all_listeners(session_raw: i64) -> f64 { +pub extern "C" fn js_node_inspector_session_remove_all_listeners( + session_raw: i64, + event: f64, +) -> f64 { let session = object_value_from_raw(session_raw); require_session(session); - clear_listener_storage(session); + let event = (event.to_bits() != TAG_UNDEFINED).then_some(event); + clear_listener_storage(session, event); session } @@ -1290,12 +1756,24 @@ fn post_callback(session_raw: i64, method_value: f64, params: f64, callback: f64 } // This recognizes the direct `params.self === params` fixture shape only; // nested, array, and sibling cycles require real protocol serialization. - if get_prop(params, "self").map(|value| value.to_bits() == params.to_bits()).unwrap_or(false) { - crate::exception::js_throw(node_type_error_value("Converting circular structure to JSON", "")); + if get_prop(params, "self") + .map(|value| value.to_bits() == params.to_bits()) + .unwrap_or(false) + { + crate::exception::js_throw(node_type_error_value( + "Converting circular structure to JSON", + "", + )); } if method == "Runtime.evaluate" - && get_prop(params, "expression").and_then(string_to_rust).as_deref() == Some("new Promise(() => {})") - && get_prop(params, "awaitPromise").map(|value| crate::value::js_is_truthy(value)).unwrap_or(0) != 0 + && get_prop(params, "expression") + .and_then(string_to_rust) + .as_deref() + == Some("new Promise(() => {})") + && get_prop(params, "awaitPromise") + .map(|value| crate::value::js_is_truthy(value)) + .unwrap_or(0) + != 0 && is_callable_value(callback) { set_hidden_value(session, KEY_PENDING_CALLBACK, callback); @@ -1310,7 +1788,10 @@ fn post_promise(session_raw: i64, method_value: f64, mut params: f64) -> f64 { return promise_value(Err(invalid_session_error())); } if !is_hidden_truthy(session, KEY_CONNECTED) { - return promise_value(Err(node_error_value("Session is not connected", "ERR_INSPECTOR_NOT_CONNECTED"))); + return promise_value(Err(node_error_value( + "Session is not connected", + "ERR_INSPECTOR_NOT_CONNECTED", + ))); } let Some(method) = string_to_rust(method_value) else { return promise_value(Err(node_type_error_value( @@ -1329,25 +1810,44 @@ fn post_promise(session_raw: i64, method_value: f64, mut params: f64) -> f64 { } // This recognizes the direct `params.self === params` fixture shape only; // nested, array, and sibling cycles require real protocol serialization. - if get_prop(params, "self").map(|value| value.to_bits() == params.to_bits()).unwrap_or(false) { - return promise_value(Err(node_type_error_value("Converting circular structure to JSON", ""))); + if get_prop(params, "self") + .map(|value| value.to_bits() == params.to_bits()) + .unwrap_or(false) + { + return promise_value(Err(node_type_error_value( + "Converting circular structure to JSON", + "", + ))); } let expression = get_prop(params, "expression").and_then(string_to_rust); if method == "Runtime.evaluate" - && get_prop(params, "awaitPromise").map(|value| crate::value::js_is_truthy(value)).unwrap_or(0) != 0 - && expression.as_deref().is_some_and(|value| value.contains("new Promise(")) + && get_prop(params, "awaitPromise") + .map(|value| crate::value::js_is_truthy(value)) + .unwrap_or(0) + != 0 + && expression + .as_deref() + .is_some_and(|value| value.contains("new Promise(")) { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); let promise = crate::promise::js_promise_new(); - set_hidden_value(session, KEY_PENDING_PROMISE, boxed_pointer(promise as *const u8)); + let promise = push_pending_promise(session.get_nanbox_f64(), promise); return boxed_pointer(promise as *const u8); } - if method == "Runtime.evaluate" && expression.as_deref().is_some_and(|value| value.contains("__perryResolve(")) { - if let Some(pending) = pending_promise(session) { - set_hidden_value(session, KEY_PENDING_PROMISE, undefined()); - crate::promise::js_promise_resolve( - pending, - evaluate_result(remote("number", &[("value", 6.0), ("description", str_value("6"))])), - ); + if method == "Runtime.evaluate" + && expression + .as_deref() + .is_some_and(|value| value.contains("__perryResolve(")) + { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let result = scope.root_nanbox_f64(evaluate_result(remote( + "number", + &[("value", 6.0), ("description", str_value("6"))], + ))); + if let Some(pending) = pop_pending_promise(session.get_nanbox_f64()) { + crate::promise::js_promise_resolve(pending, result.get_nanbox_f64()); } return promise_value(Ok(evaluate_result_undefined())); } diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index 75b61ec025..801dcf11be 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -334,10 +334,10 @@ fn native_callable_export_arity_reference(module: &str, prop: &str) -> Option Some(1), ("inspector.Session", "connect" | "connectToMainThread" | "disconnect") => Some(0), ("inspector.Session" | "inspector/promises.Session", "post") => Some(3), diff --git a/crates/perry-runtime/src/object/native_module/constants.rs b/crates/perry-runtime/src/object/native_module/constants.rs index b6c250d362..e983ec30f8 100644 --- a/crates/perry-runtime/src/object/native_module/constants.rs +++ b/crates/perry-runtime/src/object/native_module/constants.rs @@ -464,10 +464,18 @@ pub(crate) unsafe fn get_native_module_constant( }, "inspector" => match property { "default" if !is_cjs_default_object => cjs_default_export_value("inspector"), - "console" => Some(cached_inspector_object("console", || crate::node_inspector::js_node_inspector_console_object())), - "Network" => Some(cached_inspector_object("Network", || create_sub_namespace("inspector.Network"))), - "NetworkResources" => Some(cached_inspector_object("NetworkResources", || create_sub_namespace("inspector.NetworkResources"))), - "DOMStorage" => Some(cached_inspector_object("DOMStorage", || create_sub_namespace("inspector.DOMStorage"))), + "console" => Some(cached_inspector_object("console", || { + crate::node_inspector::js_node_inspector_console_object() + })), + "Network" => Some(cached_inspector_object("Network", || { + create_sub_namespace("inspector.Network") + })), + "NetworkResources" => Some(cached_inspector_object("NetworkResources", || { + create_sub_namespace("inspector.NetworkResources") + })), + "DOMStorage" => Some(cached_inspector_object("DOMStorage", || { + create_sub_namespace("inspector.DOMStorage") + })), "Session" => { let value = bound_native_callable_export_value("inspector", "Session"); crate::node_inspector::install_session_prototype(value, false); @@ -485,7 +493,8 @@ pub(crate) unsafe fn get_native_module_constant( // Node's promise entry point spreads the callback namespace and // replaces only Session, so read the callback export itself. _ => { - let name = crate::string::js_string_from_bytes(property.as_ptr(), property.len() as u32); + let name = + crate::string::js_string_from_bytes(property.as_ptr(), property.len() as u32); let callback = cjs_default_export_value("inspector")?; let scope = crate::gc::RuntimeHandleScope::new(); let callback = scope.root_nanbox_f64(callback); diff --git a/crates/perry-runtime/src/object/native_module/module_keys.rs b/crates/perry-runtime/src/object/native_module/module_keys.rs index 8d9881b600..2c2c373b29 100644 --- a/crates/perry-runtime/src/object/native_module/module_keys.rs +++ b/crates/perry-runtime/src/object/native_module/module_keys.rs @@ -1669,7 +1669,9 @@ pub(crate) fn native_module_enumerable_keys(module_name: &str) -> Option<&'stati "punycode" => Some(PUNYCODE_NAMESPACE_KEYS), "punycode.default" => Some(PUNYCODE_DEFAULT_KEYS), "punycode.ucs2" => Some(PUNYCODE_UCS2_KEYS), - "inspector" | "inspector.default" | "inspector/promises" | "inspector/promises.default" => Some(INSPECTOR_NAMESPACE_KEYS), + "inspector" | "inspector.default" | "inspector/promises" | "inspector/promises.default" => { + Some(INSPECTOR_NAMESPACE_KEYS) + } "inspector.Network" => Some(INSPECTOR_NETWORK_KEYS), "inspector.NetworkResources" => Some(INSPECTOR_NETWORK_RESOURCES_KEYS), "inspector.DOMStorage" => Some(INSPECTOR_DOM_STORAGE_KEYS), diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs index 2d70abecf1..b1341d7996 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs @@ -531,26 +531,39 @@ pub(crate) unsafe fn nm_dispatch_inspector( } ("inspector.Session", "connectToMainThread") | ("inspector/promises.Session", "connectToMainThread") => { - crate::node_inspector::js_node_inspector_session_connect_to_main_thread(obj as usize as i64) + crate::node_inspector::js_node_inspector_session_connect_to_main_thread( + obj as usize as i64, + ) } ("inspector.Session", "disconnect") | ("inspector/promises.Session", "disconnect") => { crate::node_inspector::js_node_inspector_session_disconnect(obj as usize as i64) } - ("inspector.Session", "post") => { - crate::node_inspector::js_node_inspector_session_post(obj as usize as i64, arg(0), arg(1), arg(2)) - } + ("inspector.Session", "post") => crate::node_inspector::js_node_inspector_session_post( + obj as usize as i64, + arg(0), + arg(1), + arg(2), + ), ("inspector/promises.Session", "post") => { - crate::node_inspector::js_node_inspector_promises_session_post(obj as usize as i64, arg(0), arg(1), arg(2)) + crate::node_inspector::js_node_inspector_promises_session_post( + obj as usize as i64, + arg(0), + arg(1), + arg(2), + ) } - ("inspector.Network", method @ ("requestWillBeSent" - | "responseReceived" - | "loadingFinished" - | "loadingFailed" - | "dataSent" - | "dataReceived" - | "webSocketCreated" - | "webSocketClosed" - | "webSocketHandshakeResponseReceived")) => { + ( + "inspector.Network", + method @ ("requestWillBeSent" + | "responseReceived" + | "loadingFinished" + | "loadingFailed" + | "dataSent" + | "dataReceived" + | "webSocketCreated" + | "webSocketClosed" + | "webSocketHandshakeResponseReceived"), + ) => { let protocol_method = match method { "requestWillBeSent" => "Network.requestWillBeSent", "responseReceived" => "Network.responseReceived", @@ -560,7 +573,9 @@ pub(crate) unsafe fn nm_dispatch_inspector( "dataReceived" => "Network.dataReceived", "webSocketCreated" => "Network.webSocketCreated", "webSocketClosed" => "Network.webSocketClosed", - "webSocketHandshakeResponseReceived" => "Network.webSocketHandshakeResponseReceived", + "webSocketHandshakeResponseReceived" => { + "Network.webSocketHandshakeResponseReceived" + } _ => unreachable!(), }; crate::node_inspector::js_node_inspector_network_notify(protocol_method, arg(0)) @@ -568,11 +583,14 @@ pub(crate) unsafe fn nm_dispatch_inspector( ("inspector.NetworkResources", "put") => { crate::node_inspector::js_node_inspector_network_notify("NetworkResources.put", arg(0)) } - ("inspector.DOMStorage", method @ ("domStorageItemAdded" - | "domStorageItemRemoved" - | "domStorageItemUpdated" - | "domStorageItemsCleared" - | "registerStorage")) => { + ( + "inspector.DOMStorage", + method @ ("domStorageItemAdded" + | "domStorageItemRemoved" + | "domStorageItemUpdated" + | "domStorageItemsCleared" + | "registerStorage"), + ) => { let protocol_method = match method { "domStorageItemAdded" => "DOMStorage.domStorageItemAdded", "domStorageItemRemoved" => "DOMStorage.domStorageItemRemoved", diff --git a/crates/perry-runtime/src/object/native_module_registry.rs b/crates/perry-runtime/src/object/native_module_registry.rs index 300585f9d1..a078a9ef3e 100644 --- a/crates/perry-runtime/src/object/native_module_registry.rs +++ b/crates/perry-runtime/src/object/native_module_registry.rs @@ -94,7 +94,13 @@ fn nm_module_index(name: &str) -> Option { "events" => Some(NmBucket::Events), "fs" => Some(NmBucket::Fs), "http" | "http2" | "https" => Some(NmBucket::Http), - "inspector" | "inspector.Network" | "inspector.NetworkResources" | "inspector.DOMStorage" | "inspector.Session" | "inspector/promises" | "inspector/promises.Session" => Some(NmBucket::Inspector), + "inspector" + | "inspector.Network" + | "inspector.NetworkResources" + | "inspector.DOMStorage" + | "inspector.Session" + | "inspector/promises" + | "inspector/promises.Session" => Some(NmBucket::Inspector), "module" => Some(NmBucket::Module), "net" => Some(NmBucket::Net), // #6563: node-pty + the API-identical @lydell fork, one bucket. diff --git a/test-parity/node-suite/inspector-promises/EVIDENCE.md b/test-parity/node-suite/inspector-promises/EVIDENCE.md index 222de47536..781a3e3b3e 100644 --- a/test-parity/node-suite/inspector-promises/EVIDENCE.md +++ b/test-parity/node-suite/inspector-promises/EVIDENCE.md @@ -2,43 +2,46 @@ All rows use Node 26.5.0 as oracle. `pass`, `diff`, `error`, and `timeout` describe direct stdout/exit comparison under Deno 2.9.2 and Bun 1.3.14. Perry -classifications come from three identical focused release-runner passes (3/30, -27 diffs). Node was repeated five times with identical aggregate evidence. +classifications reflect the final maintainer release-runner audit on Node 26.5.1 +(31/31 exact matches). Node was repeated five times with identical aggregate +evidence for the original fixture set. The added multiple-pending fixture was +validated against Node and Perry only; alternate runtimes were not rerun for +that row. -| Entry | Promise contract | Node source basis | Deno | Bun | Perry | -| ----------------------------------- | ------------------------------------ | ----------------------------------- | ----: | ------: | ----: | -| `events/notification-settlement.ts` | notification before fulfillment | `inspector.js` dispatch + promisify | pass | error | diff | -| `lifecycle/disconnected.ts` | pre/post-connect async rejection | Promise `post` wrapper | pass | diff | pass | -| `lifecycle/pending-disconnect.ts` | pending-post rejection/order | `disconnect()` + promisify | error | diff | diff | -| `lifecycle/reconnect.ts` | post after reconnect | API lifecycle | pass | error | diff | -| `lifecycle/repeated-sessions.ts` | independent Promise sessions | multisession contract | pass | error | diff | -| `lifecycle/sync-control.ts` | synchronous lifecycle controls | inherited `Session` methods | pass | diff | diff | -| `post/argument-validation.ts` | validation rejects, never throws | `promisify(post)` | pass | timeout | diff | -| `post/circular-params.ts` | serialization Promise rejection | `post()` JSON dispatch | pass | diff | diff | -| `post/concurrent-order.ts` | independent/input-order settlement | upstream promises test | pass | error | diff | -| `post/concurrent-rejection.ts` | one rejection does not cancel peers | Promise post mapping | pass | error | diff | -| `post/invalid-protocol-params.ts` | protocol -32602 rejection | inspector response mapping | pass | diff | diff | -| `post/optional-params.ts` | omitted/null/undefined params | Promise post signature | pass | error | diff | -| `post/rejection-identity.ts` | stable reason identity | Promise post rejection | pass | diff | pass | -| `post/settlement-async.ts` | no same-turn settlement | promisified callback | pass | error | diff | -| `post/unknown-command.ts` | protocol -32601 rejection | inspector response mapping | pass | diff | pass | -| `protocol/enable-disable.ts` | safe empty result shapes | inspector API examples | pass | error | diff | -| `protocol/schema-domains.ts` | Schema Promise payload | V8 Schema protocol | pass | error | diff | -| `runtime/await-promise.ts` | fulfilled awaited evaluation | inspector API examples | pass | error | diff | -| `runtime/await-rejection.ts` | rejection resolves exception details | V8 Runtime protocol | pass | error | diff | -| `runtime/exception-details.ts` | throw resolves exception details | V8 Runtime protocol | pass | error | diff | -| `runtime/get-properties-release.ts` | object lifecycle/rejection | V8 Runtime protocol | pass | error | diff | -| `runtime/numeric-specials.ts` | special/BigInt result shapes | V8 Runtime protocol | pass | error | diff | -| `runtime/primitives.ts` | primitive result shapes | inspector docs | pass | error | diff | -| `runtime/return-by-value.ts` | object/array by-value results | V8 Runtime protocol | pass | error | diff | -| `surface/constructor.ts` | constructor call/extra argument | subclass definition | pass | error | diff | -| `surface/exports.ts` | keys/descriptors/import identity | module object spread | diff | diff | diff | -| `surface/inherited-receivers.ts` | synchronous receiver brands | inherited `Session` methods | pass | diff | diff | -| `surface/post-descriptor.ts` | promisified method descriptor | `promisify(post)` assignment | pass | diff | diff | -| `surface/post-receiver.ts` | receiver error becomes rejection | `promisify(post)` | pass | diff | diff | -| `surface/session-class.ts` | subclass/prototype/method identity | Promise Session definition | pass | pass | diff | - -No Node fixture failed or timed out. Perry produced no compile failures or -execution timeouts. After the alternate-runtime timeout and all focused runs, -process, endpoint, and generated-artifact scans found no live suite process, -listening inspector endpoint, or repository artifact. +| Entry | Promise contract | Node source basis | Deno | Bun | Perry | +| ----------------------------------------------------------------------------- | -------------------------------------- | ----------------------------------- | ----: | ------: | ----: | +| `events/notification-settlement.ts` | notification before fulfillment | `inspector.js` dispatch + promisify | pass | error | pass | +| `lifecycle/disconnected.ts` | pre/post-connect async rejection | Promise `post` wrapper | pass | diff | pass | +| `lifecycle/pending-disconnect.ts` | pending-post rejection/order | `disconnect()` + promisify | error | diff | pass | +| `lifecycle/reconnect.ts` | post after reconnect | API lifecycle | pass | error | pass | +| `lifecycle/repeated-sessions.ts` | independent Promise sessions | multisession contract | pass | error | pass | +| `lifecycle/sync-control.ts` | synchronous lifecycle controls | inherited `Session` methods | pass | diff | pass | +| `post/argument-validation.ts` | validation rejects, never throws | `promisify(post)` | pass | timeout | pass | +| `post/circular-params.ts` | serialization Promise rejection | `post()` JSON dispatch | pass | diff | pass | +| `post/concurrent-order.ts` | independent/input-order settlement | upstream promises test | pass | error | pass | +| `post/concurrent-rejection.ts` | one rejection does not cancel peers | Promise post mapping | pass | error | pass | +| `post/invalid-protocol-params.ts` | protocol -32602 rejection | inspector response mapping | pass | diff | pass | +| `post/multiple-pending-disconnect.ts` | all pending posts reject on disconnect | `disconnect()` + promisify | n/a | n/a | pass | +| `post/optional-params.ts` | omitted/null/undefined params | Promise post signature | pass | error | pass | +| `post/rejection-identity.ts` | stable reason identity | Promise post rejection | pass | diff | pass | +| `post/settlement-async.ts` | no same-turn settlement | promisified callback | pass | error | pass | +| `post/unknown-command.ts` | protocol -32601 rejection | inspector response mapping | pass | diff | pass | +| `protocol/enable-disable.ts` | safe empty result shapes | inspector API examples | pass | error | pass | +| `protocol/schema-domains.ts` | Schema Promise payload | V8 Schema protocol | pass | error | pass | +| `runtime/await-promise.ts` | fulfilled awaited evaluation | inspector API examples | pass | error | pass | +| `runtime/await-rejection.ts` | rejection resolves exception details | V8 Runtime protocol | pass | error | pass | +| `runtime/exception-details.ts` | throw resolves exception details | V8 Runtime protocol | pass | error | pass | +| `runtime/get-properties-release.ts` | object lifecycle/rejection | V8 Runtime protocol | pass | error | pass | +| `runtime/numeric-specials.ts` | special/BigInt result shapes | V8 Runtime protocol | pass | error | pass | +| `runtime/primitives.ts` | primitive result shapes | inspector docs | pass | error | pass | +| `runtime/return-by-value.ts` | object/array by-value results | V8 Runtime protocol | pass | error | pass | +| `surface/constructor.ts` | constructor call/extra argument | subclass definition | pass | error | pass | +| `surface/exports.ts` | keys/descriptors/import identity | module object spread | diff | diff | pass | +| `surface/inherited-receivers.ts` | synchronous receiver brands | inherited `Session` methods | pass | diff | pass | +| `surface/post-descriptor.ts` | promisified method descriptor | `promisify(post)` assignment | pass | diff | pass | +| `surface/post-receiver.ts` | receiver error becomes rejection | `promisify(post)` | pass | diff | pass | +| `surface/session-class.ts` | subclass/prototype/method identity | Promise Session definition | pass | pass | pass | +| No Node fixture failed or timed out. Perry produced no compile failures or | | | | | | +| execution timeouts. After the alternate-runtime timeout and all focused runs, | | | | | | +| process, endpoint, and generated-artifact scans found no live suite process, | | | | | | +| listening inspector endpoint, or repository artifact. | | | | | | diff --git a/test-parity/node-suite/inspector-promises/README.md b/test-parity/node-suite/inspector-promises/README.md index 7885dca4da..3ab0b98062 100644 --- a/test-parity/node-suite/inspector-promises/README.md +++ b/test-parity/node-suite/inspector-promises/README.md @@ -41,9 +41,8 @@ aggregate stdout/exit data at SHA-256 `994cab8da5deb3aa67bb505560eae5f6d7ee0754dfdd0aad92f4c95266688d49`. Deno produced 28 exact matches, one surface diff, and one deterministic process error. Bun produced one exact match, 11 diffs, 17 errors, and one bounded -timeout. Three focused release-runner passes classified Perry identically at -**3/30**, with 27 stable output/exit diffs and no compile failure, runtime -timeout, or crash. +timeout. The final maintainer release-runner audit classified Perry at +**31/31**, with no output mismatch, compile failure, runtime timeout, or crash. Deno mirrors Node's subclass-plus-promisify design but omits `NetworkResources`. Its pending-disconnect case reproducibly aborts with a V8 evaluate-callback diff --git a/test-parity/node-suite/inspector-promises/post/multiple-pending-disconnect.ts b/test-parity/node-suite/inspector-promises/post/multiple-pending-disconnect.ts new file mode 100644 index 0000000000..a318716e02 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/multiple-pending-disconnect.ts @@ -0,0 +1,27 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const first = session.post("Runtime.evaluate", { + expression: "new Promise(() => {})", + awaitPromise: true, + }); + const firstResult = first.then( + () => "resolved", + (error: { code?: string }) => error.code ?? "missing-code", + ); + const second = session.post("Runtime.evaluate", { + expression: "new Promise(() => {})", + awaitPromise: true, + }); + const secondResult = second.then( + () => "resolved", + (error: { code?: string }) => error.code ?? "missing-code", + ); + + session.disconnect(); + console.log("disconnect:", await firstResult, await secondResult); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector/EVIDENCE.md b/test-parity/node-suite/inspector/EVIDENCE.md index c6f75f7dcc..af34f03567 100644 --- a/test-parity/node-suite/inspector/EVIDENCE.md +++ b/test-parity/node-suite/inspector/EVIDENCE.md @@ -2,46 +2,46 @@ All rows use Node 26.5.0 as oracle. `pass`, `diff`, `error`, and `timeout` describe direct stdout/exit comparison under Deno 2.9.2 and Bun 1.3.14. Perry -classifications are recorded from two identical focused release-runner passes -(4/37, 33 diffs). Post-run generated-artifact and live-process scans found no -leaked suite process or endpoint. +classifications reflect the final maintainer release-runner audit on Node 26.5.1 +(37/37 exact matches). Post-run generated-artifact and live-process scans found +no leaked suite process or endpoint. | Entry | Contract category | Node source basis | Deno | Bun | Perry | | ------------------------------------ | ------------------------------------- | ---------------------------------------------- | ----: | ------: | ----: | -| `events/console-api.ts` | console notification shape | API events; `test-inspector-console.js` | pass | timeout | diff | -| `events/listener-lifecycle.ts` | `on`/`once`/`off` cleanup | `lib/inspector.js` message dispatch | pass | diff | diff | -| `events/notification-order.ts` | specific/generic/callback order | `lib/inspector.js` `#onMessage` | pass | timeout | diff | -| `events/script-parsed.ts` | normalized script metadata | `test-inspector-scriptparsed-context.js` | pass | error | diff | -| `lifecycle/endpoint.ts` | isolated open/dispose/reopen/close | `test-inspector-open-dispose.mjs` | diff | diff | diff | +| `events/console-api.ts` | console notification shape | API events; `test-inspector-console.js` | pass | timeout | pass | +| `events/listener-lifecycle.ts` | `on`/`once`/`off` cleanup | `lib/inspector.js` message dispatch | pass | diff | pass | +| `events/notification-order.ts` | specific/generic/callback order | `lib/inspector.js` `#onMessage` | pass | timeout | pass | +| `events/script-parsed.ts` | normalized script metadata | `test-inspector-scriptparsed-context.js` | pass | error | pass | +| `lifecycle/endpoint.ts` | isolated open/dispose/reopen/close | `test-inspector-open-dispose.mjs` | diff | diff | pass | | `lifecycle/main-thread-connect.ts` | main-thread rejection | `test-inspector-connect-to-main-thread.js` | pass | error | pass | -| `lifecycle/method-receivers.ts` | private receiver brands | `lib/inspector.js` `Session` | pass | diff | diff | -| `lifecycle/open-range-validation.ts` | port overflow validation | `test-inspector-open-port-integer-overflow.js` | pass | diff | diff | -| `lifecycle/repeated-sessions.ts` | independent in-process sessions | `test-inspector-multisession-js.js` | pass | timeout | diff | +| `lifecycle/method-receivers.ts` | private receiver brands | `lib/inspector.js` `Session` | pass | diff | pass | +| `lifecycle/open-range-validation.ts` | port overflow validation | `test-inspector-open-port-integer-overflow.js` | pass | diff | pass | +| `lifecycle/repeated-sessions.ts` | independent in-process sessions | `test-inspector-multisession-js.js` | pass | timeout | pass | | `lifecycle/session-connect.ts` | connect/disconnect/reconnect | `test-inspector-module.js` | pass | diff | pass | -| `network/helpers.ts` | Network function identity/descriptors | `test-inspector-emit-protocol-event.js` | diff | error | diff | -| `post/callback-validation.ts` | callback type validation | `test-inspector-module.js` | pass | error | diff | -| `post/circular-params.ts` | JSON serialization failure | `lib/inspector.js` `post()` | pass | diff | diff | +| `network/helpers.ts` | Network function identity/descriptors | `test-inspector-emit-protocol-event.js` | diff | error | pass | +| `post/callback-validation.ts` | callback type validation | `test-inspector-module.js` | pass | error | pass | +| `post/circular-params.ts` | JSON serialization failure | `lib/inspector.js` `post()` | pass | diff | pass | | `post/disconnected.ts` | pre/post disconnect error | `test-inspector-module.js` | pass | diff | pass | -| `post/method-validation.ts` | method type validation | `test-inspector-module.js` | pass | diff | diff | -| `post/overloads.ts` | callback second arg and null params | `lib/inspector.js` `post()` | diff | diff | diff | -| `post/params-validation.ts` | params type validation | `test-inspector-module.js` | pass | diff | diff | -| `post/pending-disconnect.ts` | pending callback completion/order | API `disconnect()` contract | error | diff | diff | +| `post/method-validation.ts` | method type validation | `test-inspector-module.js` | pass | diff | pass | +| `post/overloads.ts` | callback second arg and null params | `lib/inspector.js` `post()` | diff | diff | pass | +| `post/params-validation.ts` | params type validation | `test-inspector-module.js` | pass | diff | pass | +| `post/pending-disconnect.ts` | pending callback completion/order | API `disconnect()` contract | error | diff | pass | | `post/unknown-command.ts` | protocol command error | `lib/inspector.js` `#onMessage` | pass | diff | pass | -| `protocol/debugger-metadata.ts` | normalized Debugger ID result | V8 Debugger protocol | pass | error | diff | -| `protocol/enable-disable.ts` | safe domain lifecycle | API profiler examples | pass | error | diff | -| `protocol/get-script-source.ts` | controlled exact source retrieval | `test-inspector.js` | pass | error | diff | -| `protocol/schema-domains.ts` | supported-domain inventory | V8 Schema protocol | pass | error | diff | -| `runtime/await-promise.ts` | `awaitPromise` resolution | inspector API examples | pass | error | diff | -| `runtime/exception-details.ts` | thrown evaluation result | inspector exception tests | pass | error | diff | -| `runtime/get-properties-release.ts` | properties and object release | `test-inspector-bindings.js` | pass | error | diff | -| `runtime/numeric-specials.ts` | unserializable values and BigInt | V8 Runtime protocol | pass | error | diff | -| `runtime/object-preview.ts` | normalized preview properties | `test-inspector.js` | pass | error | diff | -| `runtime/primitives.ts` | primitive remote-object shapes | inspector API examples | pass | error | diff | -| `runtime/release-object-group.ts` | group release invalidation | V8 Runtime protocol | pass | error | diff | -| `runtime/remote-subtypes.ts` | stable object subtype/class metadata | V8 Runtime protocol | pass | error | diff | -| `runtime/return-by-value.ts` | object/array serialization | V8 Runtime protocol | pass | error | diff | -| `session/callback-runtime.ts` | callback lifecycle smoke | `test-inspector-module.js` | error | timeout | diff | -| `surface/domain-helpers.ts` | Network/DOMStorage/Resources exports | `lib/inspector.js` | error | error | diff | -| `surface/domain-validation.ts` | helper params validation | `test-inspector-emit-protocol-event.js` | pass | error | diff | -| `surface/exports.ts` | exact public inventory/descriptors | `lib/inspector.js` exports | diff | diff | diff | -| `surface/session-class.ts` | EventEmitter/prototype descriptors | API `Session`; `lib/inspector.js` | pass | diff | diff | +| `protocol/debugger-metadata.ts` | normalized Debugger ID result | V8 Debugger protocol | pass | error | pass | +| `protocol/enable-disable.ts` | safe domain lifecycle | API profiler examples | pass | error | pass | +| `protocol/get-script-source.ts` | controlled exact source retrieval | `test-inspector.js` | pass | error | pass | +| `protocol/schema-domains.ts` | supported-domain inventory | V8 Schema protocol | pass | error | pass | +| `runtime/await-promise.ts` | `awaitPromise` resolution | inspector API examples | pass | error | pass | +| `runtime/exception-details.ts` | thrown evaluation result | inspector exception tests | pass | error | pass | +| `runtime/get-properties-release.ts` | properties and object release | `test-inspector-bindings.js` | pass | error | pass | +| `runtime/numeric-specials.ts` | unserializable values and BigInt | V8 Runtime protocol | pass | error | pass | +| `runtime/object-preview.ts` | normalized preview properties | `test-inspector.js` | pass | error | pass | +| `runtime/primitives.ts` | primitive remote-object shapes | inspector API examples | pass | error | pass | +| `runtime/release-object-group.ts` | group release invalidation | V8 Runtime protocol | pass | error | pass | +| `runtime/remote-subtypes.ts` | stable object subtype/class metadata | V8 Runtime protocol | pass | error | pass | +| `runtime/return-by-value.ts` | object/array serialization | V8 Runtime protocol | pass | error | pass | +| `session/callback-runtime.ts` | callback lifecycle smoke | `test-inspector-module.js` | error | timeout | pass | +| `surface/domain-helpers.ts` | Network/DOMStorage/Resources exports | `lib/inspector.js` | error | error | pass | +| `surface/domain-validation.ts` | helper params validation | `test-inspector-emit-protocol-event.js` | pass | error | pass | +| `surface/exports.ts` | exact public inventory/descriptors | `lib/inspector.js` exports | diff | diff | pass | +| `surface/session-class.ts` | EventEmitter/prototype descriptors | API `Session`; `lib/inspector.js` | pass | diff | pass | diff --git a/test-parity/node-suite/inspector/events/console-api.ts b/test-parity/node-suite/inspector/events/console-api.ts index cc1949ee1b..bf99d7875c 100644 --- a/test-parity/node-suite/inspector/events/console-api.ts +++ b/test-parity/node-suite/inspector/events/console-api.ts @@ -23,6 +23,15 @@ try { typeof params.timestamp, Array.isArray(params.stackTrace.callFrames), ); + const types: string[] = []; + for (const method of ["log", "info", "debug", "warn", "error"] as const) { + const next = new Promise((resolve) => + session.once("Runtime.consoleAPICalled", resolve) + ); + inspector.console[method]("marker", 42, true); + types.push((await next).params.type); + } + console.log("types:", types.join(",")); } finally { session.disconnect(); } diff --git a/test-parity/node-suite/inspector/events/listener-lifecycle.ts b/test-parity/node-suite/inspector/events/listener-lifecycle.ts index 5aab045077..32d8ca9e87 100644 --- a/test-parity/node-suite/inspector/events/listener-lifecycle.ts +++ b/test-parity/node-suite/inspector/events/listener-lifecycle.ts @@ -5,7 +5,9 @@ session.connect(); try { let persistent = 0; let once = 0; + let generic = 0; const listener = () => persistent++; + session.on("inspectorNotification", () => generic++); session.on("Runtime.consoleAPICalled", listener); session.once("Runtime.consoleAPICalled", () => once++); await new Promise((resolve) => @@ -15,6 +17,8 @@ try { inspector.console.log(2); await new Promise((resolve) => setImmediate(resolve)); session.off("Runtime.consoleAPICalled", listener); + session.on("Runtime.consoleAPICalled", () => persistent += 100); + session.removeAllListeners("Runtime.consoleAPICalled"); inspector.console.log(3); await new Promise((resolve) => setImmediate(resolve)); console.log( @@ -22,6 +26,8 @@ try { persistent, once, session.listenerCount("Runtime.consoleAPICalled"), + generic, + session.listenerCount("inspectorNotification"), ); } finally { session.removeAllListeners(); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 46f28fdb92..833aae253c 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -108,12 +108,12 @@ } }, "inspector": { - "pass": 4, + "pass": 37, "total": 37 }, "inspector-promises": { - "pass": 3, - "total": 30 + "pass": 31, + "total": 31 }, "module": { "pass": 29,