fix(ext/node): better inspector support (#26471)

implement local inspector

future changes:
- wire up InspectorServer to enable open/close/url
- wire up connectToMainThread

Fixes https://github.com/denoland/deno/issues/25004
This commit is contained in:
snek 2024-11-06 15:08:26 +01:00 committed by GitHub
parent 64e887083a
commit 700f54a13c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 438 additions and 98 deletions

View File

@ -47,6 +47,11 @@ pub trait NodePermissions {
url: &Url,
api_name: &str,
) -> Result<(), PermissionCheckError>;
fn check_net(
&mut self,
host: (&str, Option<u16>),
api_name: &str,
) -> Result<(), PermissionCheckError>;
#[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"]
#[inline(always)]
fn check_read(
@ -90,6 +95,14 @@ impl NodePermissions for deno_permissions::PermissionsContainer {
deno_permissions::PermissionsContainer::check_net_url(self, url, api_name)
}
fn check_net(
&mut self,
host: (&str, Option<u16>),
api_name: &str,
) -> Result<(), PermissionCheckError> {
deno_permissions::PermissionsContainer::check_net(self, &host, api_name)
}
#[inline(always)]
fn check_read_with_api_name(
&mut self,
@ -398,6 +411,15 @@ deno_core::extension!(deno_node,
ops::process::op_node_process_kill,
ops::process::op_process_abort,
ops::tls::op_get_root_certificates,
ops::inspector::op_inspector_open<P>,
ops::inspector::op_inspector_close,
ops::inspector::op_inspector_url,
ops::inspector::op_inspector_wait,
ops::inspector::op_inspector_connect<P>,
ops::inspector::op_inspector_dispatch,
ops::inspector::op_inspector_disconnect,
ops::inspector::op_inspector_emit_protocol_event,
ops::inspector::op_inspector_enabled,
],
esm_entry_point = "ext:deno_node/02_init.js",
esm = [
@ -606,8 +628,8 @@ deno_core::extension!(deno_node,
"node:http" = "http.ts",
"node:http2" = "http2.ts",
"node:https" = "https.ts",
"node:inspector" = "inspector.ts",
"node:inspector/promises" = "inspector.ts",
"node:inspector" = "inspector.js",
"node:inspector/promises" = "inspector/promises.js",
"node:module" = "01_require.js",
"node:net" = "net.ts",
"node:os" = "os.ts",

161
ext/node/ops/inspector.rs Normal file
View File

@ -0,0 +1,161 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use crate::NodePermissions;
use deno_core::anyhow::Error;
use deno_core::error::generic_error;
use deno_core::futures::channel::mpsc;
use deno_core::op2;
use deno_core::v8;
use deno_core::GarbageCollected;
use deno_core::InspectorSessionKind;
use deno_core::InspectorSessionOptions;
use deno_core::JsRuntimeInspector;
use deno_core::OpState;
use std::cell::RefCell;
use std::rc::Rc;
#[op2(fast)]
pub fn op_inspector_enabled() -> bool {
// TODO: hook up to InspectorServer
false
}
#[op2]
pub fn op_inspector_open<P>(
_state: &mut OpState,
_port: Option<u16>,
#[string] _host: Option<String>,
) -> Result<(), Error>
where
P: NodePermissions + 'static,
{
// TODO: hook up to InspectorServer
/*
let server = state.borrow_mut::<InspectorServer>();
if let Some(host) = host {
server.set_host(host);
}
if let Some(port) = port {
server.set_port(port);
}
state
.borrow_mut::<P>()
.check_net((server.host(), Some(server.port())), "inspector.open")?;
*/
Ok(())
}
#[op2(fast)]
pub fn op_inspector_close() {
// TODO: hook up to InspectorServer
}
#[op2]
#[string]
pub fn op_inspector_url() -> Option<String> {
// TODO: hook up to InspectorServer
None
}
#[op2(fast)]
pub fn op_inspector_wait(state: &OpState) -> bool {
match state.try_borrow::<Rc<RefCell<JsRuntimeInspector>>>() {
Some(inspector) => {
inspector
.borrow_mut()
.wait_for_session_and_break_on_next_statement();
true
}
None => false,
}
}
#[op2(fast)]
pub fn op_inspector_emit_protocol_event(
#[string] _event_name: String,
#[string] _params: String,
) {
// TODO: inspector channel & protocol notifications
}
struct JSInspectorSession {
tx: RefCell<Option<mpsc::UnboundedSender<String>>>,
}
impl GarbageCollected for JSInspectorSession {}
#[op2]
#[cppgc]
pub fn op_inspector_connect<'s, P>(
isolate: *mut v8::Isolate,
scope: &mut v8::HandleScope<'s>,
state: &mut OpState,
connect_to_main_thread: bool,
callback: v8::Local<'s, v8::Function>,
) -> Result<JSInspectorSession, Error>
where
P: NodePermissions + 'static,
{
state
.borrow_mut::<P>()
.check_sys("inspector", "inspector.Session.connect")?;
if connect_to_main_thread {
return Err(generic_error("connectToMainThread not supported"));
}
let context = scope.get_current_context();
let context = v8::Global::new(scope, context);
let callback = v8::Global::new(scope, callback);
let inspector = state
.borrow::<Rc<RefCell<JsRuntimeInspector>>>()
.borrow_mut();
let tx = inspector.create_raw_session(
InspectorSessionOptions {
kind: InspectorSessionKind::NonBlocking {
wait_for_disconnect: false,
},
},
// The inspector connection does not keep the event loop alive but
// when the inspector sends a message to the frontend, the JS that
// that runs may keep the event loop alive so we have to call back
// synchronously, instead of using the usual LocalInspectorSession
// UnboundedReceiver<InspectorMsg> API.
Box::new(move |message| {
// SAFETY: This function is called directly by the inspector, so
// 1) The isolate is still valid
// 2) We are on the same thread as the Isolate
let scope = unsafe { &mut v8::CallbackScope::new(&mut *isolate) };
let context = v8::Local::new(scope, context.clone());
let scope = &mut v8::ContextScope::new(scope, context);
let scope = &mut v8::TryCatch::new(scope);
let recv = v8::undefined(scope);
if let Some(message) = v8::String::new(scope, &message.content) {
let callback = v8::Local::new(scope, callback.clone());
callback.call(scope, recv.into(), &[message.into()]);
}
}),
);
Ok(JSInspectorSession {
tx: RefCell::new(Some(tx)),
})
}
#[op2(fast)]
pub fn op_inspector_dispatch(
#[cppgc] session: &JSInspectorSession,
#[string] message: String,
) {
if let Some(tx) = &*session.tx.borrow() {
let _ = tx.unbounded_send(message);
}
}
#[op2(fast)]
pub fn op_inspector_disconnect(#[cppgc] session: &JSInspectorSession) {
drop(session.tx.borrow_mut().take());
}

View File

@ -7,6 +7,7 @@ pub mod fs;
pub mod http;
pub mod http2;
pub mod idna;
pub mod inspector;
pub mod ipc;
pub mod os;
pub mod process;

View File

@ -0,0 +1,210 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
import process from "node:process";
import { EventEmitter } from "node:events";
import { primordials } from "ext:core/mod.js";
import {
op_get_extras_binding_object,
op_inspector_close,
op_inspector_connect,
op_inspector_disconnect,
op_inspector_dispatch,
op_inspector_emit_protocol_event,
op_inspector_enabled,
op_inspector_open,
op_inspector_url,
op_inspector_wait,
} from "ext:core/ops";
import {
isUint32,
validateFunction,
validateInt32,
validateObject,
validateString,
} from "ext:deno_node/internal/validators.mjs";
import {
ERR_INSPECTOR_ALREADY_ACTIVATED,
ERR_INSPECTOR_ALREADY_CONNECTED,
ERR_INSPECTOR_CLOSED,
ERR_INSPECTOR_COMMAND,
ERR_INSPECTOR_NOT_ACTIVE,
ERR_INSPECTOR_NOT_CONNECTED,
ERR_INSPECTOR_NOT_WORKER,
} from "ext:deno_node/internal/errors.ts";
const {
SymbolDispose,
JSONParse,
JSONStringify,
SafeMap,
} = primordials;
class Session extends EventEmitter {
#connection = null;
#nextId = 1;
#messageCallbacks = new SafeMap();
connect() {
if (this.#connection) {
throw new ERR_INSPECTOR_ALREADY_CONNECTED("The inspector session");
}
this.#connection = op_inspector_connect(false, (m) => this.#onMessage(m));
}
connectToMainThread() {
if (isMainThread) {
throw new ERR_INSPECTOR_NOT_WORKER();
}
if (this.#connection) {
throw new ERR_INSPECTOR_ALREADY_CONNECTED("The inspector session");
}
this.#connection = op_inspector_connect(true, (m) => this.#onMessage(m));
}
#onMessage(message) {
const parsed = JSONParse(message);
try {
if (parsed.id) {
const callback = this.#messageCallbacks.get(parsed.id);
this.#messageCallbacks.delete(parsed.id);
if (callback) {
if (parsed.error) {
return callback(
new ERR_INSPECTOR_COMMAND(
parsed.error.code,
parsed.error.message,
),
);
}
callback(null, parsed.result);
}
} else {
this.emit(parsed.method, parsed);
this.emit("inspectorNotification", parsed);
}
} catch (error) {
process.emitWarning(error);
}
}
post(method, params, callback) {
validateString(method, "method");
if (!callback && typeof params === "function") {
callback = params;
params = null;
}
if (params) {
validateObject(params, "params");
}
if (callback) {
validateFunction(callback, "callback");
}
if (!this.#connection) {
throw new ERR_INSPECTOR_NOT_CONNECTED();
}
const id = this.#nextId++;
const message = { id, method };
if (params) {
message.params = params;
}
if (callback) {
this.#messageCallbacks.set(id, callback);
}
op_inspector_dispatch(this.#connection, JSONStringify(message));
}
disconnect() {
if (!this.#connection) {
return;
}
op_inspector_disconnect(this.#connection);
this.#connection = null;
// deno-lint-ignore prefer-primordials
for (const callback of this.#messageCallbacks.values()) {
process.nextTick(callback, new ERR_INSPECTOR_CLOSED());
}
this.#messageCallbacks.clear();
this.#nextId = 1;
}
}
function open(port, host, wait) {
if (op_inspector_enabled()) {
throw new ERR_INSPECTOR_ALREADY_ACTIVATED();
}
// inspectorOpen() currently does not typecheck its arguments and adding
// such checks would be a potentially breaking change. However, the native
// open() function requires the port to fit into a 16-bit unsigned integer,
// causing an integer overflow otherwise, so we at least need to prevent that.
if (isUint32(port)) {
validateInt32(port, "port", 0, 65535);
} else {
// equiv of handling args[0]->IsUint32()
port = undefined;
}
if (typeof host !== "string") {
// equiv of handling args[1]->IsString()
host = undefined;
}
op_inspector_open(port, host);
if (wait) {
op_inspector_wait();
}
return {
__proto__: null,
[SymbolDispose]() {
_debugEnd();
},
};
}
function close() {
op_inspector_close();
}
function url() {
return op_inspector_url();
}
function waitForDebugger() {
if (!op_inspector_wait()) {
throw new ERR_INSPECTOR_NOT_ACTIVE();
}
}
function broadcastToFrontend(eventName, params) {
validateString(eventName, "eventName");
if (params) {
validateObject(params, "params");
}
op_inspector_emit_protocol_event(eventName, JSONStringify(params ?? {}));
}
const Network = {
requestWillBeSent: (params) =>
broadcastToFrontend("Network.requestWillBeSent", params),
responseReceived: (params) =>
broadcastToFrontend("Network.responseReceived", params),
loadingFinished: (params) =>
broadcastToFrontend("Network.loadingFinished", params),
loadingFailed: (params) =>
broadcastToFrontend("Network.loadingFailed", params),
};
const console = op_get_extras_binding_object().console;
export { close, console, Network, open, Session, url, waitForDebugger };
export default {
open,
close,
url,
waitForDebugger,
console,
Session,
Network,
};

View File

@ -1,82 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
import { EventEmitter } from "node:events";
import { notImplemented } from "ext:deno_node/_utils.ts";
import { primordials } from "ext:core/mod.js";
const {
SafeMap,
} = primordials;
class Session extends EventEmitter {
#connection = null;
#nextId = 1;
#messageCallbacks = new SafeMap();
/** Connects the session to the inspector back-end. */
connect() {
notImplemented("inspector.Session.prototype.connect");
}
/** Connects the session to the main thread
* inspector back-end. */
connectToMainThread() {
notImplemented("inspector.Session.prototype.connectToMainThread");
}
/** Posts a message to the inspector back-end. */
post(
_method: string,
_params?: Record<string, unknown>,
_callback?: (...args: unknown[]) => void,
) {
notImplemented("inspector.Session.prototype.post");
}
/** Immediately closes the session, all pending
* message callbacks will be called with an
* error.
*/
disconnect() {
notImplemented("inspector.Session.prototype.disconnect");
}
}
/** Activates inspector on host and port.
* See https://nodejs.org/api/inspector.html#inspectoropenport-host-wait */
function open(_port?: number, _host?: string, _wait?: boolean) {
notImplemented("inspector.Session.prototype.open");
}
/** Deactivate the inspector. Blocks until there are no active connections.
* See https://nodejs.org/api/inspector.html#inspectorclose */
function close() {
notImplemented("inspector.Session.prototype.close");
}
/** Return the URL of the active inspector, or undefined if there is none.
* See https://nodejs.org/api/inspector.html#inspectorurl */
function url() {
// TODO(kt3k): returns undefined for now, which means the inspector is not activated.
return undefined;
}
/** Blocks until a client (existing or connected later) has sent Runtime.runIfWaitingForDebugger command.
* See https://nodejs.org/api/inspector.html#inspectorwaitfordebugger */
function waitForDebugger() {
notImplemented("inspector.wairForDebugger");
}
const console = globalThis.console;
export { close, console, open, Session, url, waitForDebugger };
export default {
close,
console,
open,
Session,
url,
waitForDebugger,
};

View File

@ -0,0 +1,20 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
import inspector from "node:inspector";
import { promisify } from "ext:deno_node/internal/util.mjs";
class Session extends inspector.Session {
constructor() {
super();
}
}
Session.prototype.post = promisify(inspector.Session.prototype.post);
export * from "node:inspector";
export { Session };
export default {
...inspector,
Session,
};

View File

@ -82,6 +82,13 @@ impl deno_node::NodePermissions for Permissions {
) -> Result<(), PermissionCheckError> {
unreachable!("snapshotting!")
}
fn check_net(
&mut self,
_host: (&str, Option<u16>),
_api_name: &str,
) -> Result<(), PermissionCheckError> {
unreachable!("snapshotting!")
}
fn check_read_path<'a>(
&mut self,
_path: &'a Path,

View File

@ -562,7 +562,7 @@ impl WebWorker {
extension_transpiler: Some(Rc::new(|specifier, source| {
maybe_transpile_source(specifier, source)
})),
inspector: services.maybe_inspector_server.is_some(),
inspector: true,
feature_checker: Some(services.feature_checker),
op_metrics_factory_fn,
import_meta_resolve_callback: Some(Box::new(
@ -579,18 +579,18 @@ impl WebWorker {
js_runtime.op_state().borrow_mut().put(op_summary_metrics);
}
// Put inspector handle into the op state so we can put a breakpoint when
// executing a CJS entrypoint.
let op_state = js_runtime.op_state();
let inspector = js_runtime.inspector();
op_state.borrow_mut().put(inspector);
if let Some(server) = services.maybe_inspector_server {
server.register_inspector(
options.main_module.to_string(),
&mut js_runtime,
false,
);
// Put inspector handle into the op state so we can put a breakpoint when
// executing a CJS entrypoint.
let op_state = js_runtime.op_state();
let inspector = js_runtime.inspector();
op_state.borrow_mut().put(inspector);
}
let (internal_handle, external_handle) = {

View File

@ -488,7 +488,7 @@ impl MainWorker {
extension_transpiler: Some(Rc::new(|specifier, source| {
maybe_transpile_source(specifier, source)
})),
inspector: options.maybe_inspector_server.is_some(),
inspector: true,
is_main: true,
feature_checker: Some(services.feature_checker.clone()),
op_metrics_factory_fn,
@ -546,6 +546,12 @@ impl MainWorker {
js_runtime.op_state().borrow_mut().put(op_summary_metrics);
}
// Put inspector handle into the op state so we can put a breakpoint when
// executing a CJS entrypoint.
let op_state = js_runtime.op_state();
let inspector = js_runtime.inspector();
op_state.borrow_mut().put(inspector);
if let Some(server) = options.maybe_inspector_server.clone() {
server.register_inspector(
main_module.to_string(),
@ -553,13 +559,8 @@ impl MainWorker {
options.should_break_on_first_statement
|| options.should_wait_for_inspector_session,
);
// Put inspector handle into the op state so we can put a breakpoint when
// executing a CJS entrypoint.
let op_state = js_runtime.op_state();
let inspector = js_runtime.inspector();
op_state.borrow_mut().put(inspector);
}
let (
bootstrap_fn_global,
dispatch_load_event_fn_global,