Skip to content

Commit c0ebacb

Browse files
authored
fix(ext/node): handle multiple calls in inspector.Session.post() (#31067)
Fixes a panic that occurred when using nested session.post() calls in the node:inspector module. The issue manifested when callbacks from inspector commands (like Profiler.enable) called session.post() again, causing a re-entrant invocation of op_inspector_dispatch. Fixes #31020
1 parent 5cee3a3 commit c0ebacb

File tree

3 files changed

+118
-8
lines changed

3 files changed

+118
-8
lines changed

ext/node/ops/inspector.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,17 +167,17 @@ where
167167
})
168168
}
169169

170-
#[op2(fast)]
170+
#[op2(fast, reentrant)]
171171
pub fn op_inspector_dispatch(
172-
#[cppgc] session: &JSInspectorSession,
172+
#[cppgc] inspector: &JSInspectorSession,
173173
#[string] message: String,
174174
) {
175-
if let Some(session) = &mut *session.session.borrow_mut() {
175+
if let Some(session) = &mut *inspector.session.borrow_mut() {
176176
session.dispatch(message);
177177
}
178178
}
179179

180180
#[op2(fast)]
181-
pub fn op_inspector_disconnect(#[cppgc] session: &JSInspectorSession) {
182-
drop(session.session.borrow_mut().take());
181+
pub fn op_inspector_disconnect(#[cppgc] inspector: &JSInspectorSession) {
182+
inspector.session.borrow_mut().take();
183183
}

ext/node/polyfills/inspector.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
} from "ext:deno_node/internal/errors.ts";
3535

3636
const {
37+
ArrayPrototypePush,
38+
ArrayPrototypeShift,
3739
SymbolDispose,
3840
JSONParse,
3941
JSONStringify,
@@ -44,12 +46,18 @@ class Session extends EventEmitter {
4446
#connection = null;
4547
#nextId = 1;
4648
#messageCallbacks = new SafeMap();
49+
#pendingMessages = [];
50+
#drainScheduled = false;
51+
#isDraining = false;
4752

4853
connect() {
4954
if (this.#connection) {
5055
throw new ERR_INSPECTOR_ALREADY_CONNECTED("The inspector session");
5156
}
52-
this.#connection = op_inspector_connect(false, (m) => this.#onMessage(m));
57+
this.#connection = op_inspector_connect(
58+
false,
59+
(m) => this.#enqueueMessage(m),
60+
);
5361
}
5462

5563
connectToMainThread() {
@@ -59,7 +67,10 @@ class Session extends EventEmitter {
5967
if (this.#connection) {
6068
throw new ERR_INSPECTOR_ALREADY_CONNECTED("The inspector session");
6169
}
62-
this.#connection = op_inspector_connect(true, (m) => this.#onMessage(m));
70+
this.#connection = op_inspector_connect(
71+
true,
72+
(m) => this.#enqueueMessage(m),
73+
);
6374
}
6475

6576
#onMessage(message) {
@@ -89,6 +100,28 @@ class Session extends EventEmitter {
89100
}
90101
}
91102

103+
#enqueueMessage(message) {
104+
ArrayPrototypePush(this.#pendingMessages, message);
105+
if (this.#isDraining) return;
106+
if (!this.#drainScheduled) {
107+
this.#drainScheduled = true;
108+
process.nextTick(() => this.#drainMessages());
109+
}
110+
}
111+
112+
#drainMessages() {
113+
this.#drainScheduled = false;
114+
this.#isDraining = true;
115+
try {
116+
while (this.#pendingMessages.length > 0) {
117+
const nextMessage = ArrayPrototypeShift(this.#pendingMessages);
118+
this.#onMessage(nextMessage);
119+
}
120+
} finally {
121+
this.#isDraining = false;
122+
}
123+
}
124+
92125
post(method, params, callback) {
93126
validateString(method, "method");
94127
if (!callback && typeof params === "function") {
@@ -128,6 +161,9 @@ class Session extends EventEmitter {
128161
}
129162
this.#messageCallbacks.clear();
130163
this.#nextId = 1;
164+
this.#pendingMessages.length = 0;
165+
this.#drainScheduled = false;
166+
this.#isDraining = false;
131167
}
132168
}
133169

tests/unit_node/inspector_test.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import inspector, { Session } from "node:inspector";
33
import inspectorPromises, {
44
Session as SessionPromise,
55
} from "node:inspector/promises";
6-
import { assertEquals } from "@std/assert/equals";
6+
import { assertEquals } from "@std/assert";
77

88
Deno.test("[node/inspector] - importing inspector works", () => {
99
assertEquals(typeof inspector.open, "function");
@@ -20,3 +20,77 @@ Deno.test("[node/inspector/promises] - importing inspector works", () => {
2020
Deno.test("[node/inspector/promises] - Session constructor should not throw", () => {
2121
new SessionPromise();
2222
});
23+
24+
// Regression test for: https://github.com/denoland/deno/issues/31020
25+
Deno.test({
26+
name: "[node/inspector] - deeply nested session.post() calls",
27+
fn: async () => {
28+
const session = new Session();
29+
session.connect();
30+
31+
const results: number[] = [];
32+
33+
await new Promise<void>((resolve) => {
34+
session.post("Profiler.enable", () => {
35+
results.push(1);
36+
session.post("Profiler.start", () => {
37+
results.push(2);
38+
session.post("Profiler.stop", () => {
39+
results.push(3);
40+
session.post("Profiler.disable", () => {
41+
results.push(4);
42+
session.disconnect();
43+
resolve();
44+
});
45+
});
46+
});
47+
});
48+
});
49+
50+
assertEquals(results, [1, 2, 3, 4]);
51+
},
52+
});
53+
54+
Deno.test({
55+
name:
56+
"[node/inspector] - multiple session.post() calls in same callback are queued",
57+
fn: async () => {
58+
const session = new Session();
59+
session.connect();
60+
61+
const results: string[] = [];
62+
63+
await new Promise<void>((resolve) => {
64+
session.post("Profiler.enable", () => {
65+
results.push("enable-callback-start");
66+
67+
// Make multiple session.post() calls in the same callback
68+
// These should be queued and processed in order
69+
session.post("Profiler.start", () => {
70+
results.push("start-callback");
71+
});
72+
73+
session.post("Profiler.stop", () => {
74+
results.push("stop-callback");
75+
});
76+
77+
session.post("Profiler.disable", () => {
78+
results.push("disable-callback");
79+
session.disconnect();
80+
resolve();
81+
});
82+
83+
results.push("enable-callback-end");
84+
});
85+
});
86+
87+
// Verify the outer callback completes first, then queued messages are processed
88+
assertEquals(results, [
89+
"enable-callback-start",
90+
"enable-callback-end",
91+
"start-callback",
92+
"stop-callback",
93+
"disable-callback",
94+
]);
95+
},
96+
});

0 commit comments

Comments
 (0)