Skip to content

Commit 2cc24e5

Browse files
committed
debugger: wait for target startup before initialization
The debugger endpoint can accept a connection before the target has entered its startup wait. In that window,Runtime.runIfWaitingForDebugger may be handled too early, leaving the target waiting indefinitely. Use NodeRuntime.waitingForDebugger as a readiness handshake before initializing the debugger domains and releasing the target. Apply the handshake to both the interactive debugger and probe mode,and reject the wait if the session closes. Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com>
1 parent c18fd90 commit 2cc24e5

4 files changed

Lines changed: 99 additions & 3 deletions

File tree

‎lib/internal/debugger/inspect_helpers.js‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ const {
44
ArrayPrototypePushApply,
55
Number,
66
Promise,
7+
PromiseWithResolvers,
78
RegExpPrototypeExec,
9+
SafePromiseRace,
810
StringPrototypeEndsWith,
911
} = primordials;
1012

@@ -18,7 +20,10 @@ const {
1820
AbortController,
1921
} = require('internal/abort_controller');
2022

21-
const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes;
23+
const {
24+
ERR_DEBUGGER_ERROR,
25+
ERR_DEBUGGER_STARTUP_ERROR,
26+
} = require('internal/errors').codes;
2227
const {
2328
exitCodes: {
2429
kInvalidCommandLineArgument,
@@ -61,6 +66,41 @@ function ensureTrailingNewline(text) {
6166
return StringPrototypeEndsWith(text, '\n') ? text : `${text}\n`;
6267
}
6368

69+
async function waitForDebugger(
70+
client,
71+
callMethod = (method) => client.callMethod(method),
72+
) {
73+
const {
74+
promise: waitingPromise,
75+
resolve: resolveWaiting,
76+
} = PromiseWithResolvers();
77+
const {
78+
promise: closedPromise,
79+
reject: rejectClosed,
80+
} = PromiseWithResolvers();
81+
const onWaiting = () => resolveWaiting();
82+
const onClose = () => {
83+
rejectClosed(new ERR_DEBUGGER_ERROR(
84+
'Debugger session ended while waiting for target startup'));
85+
};
86+
87+
client.once('NodeRuntime.waitingForDebugger', onWaiting);
88+
client.once('close', onClose);
89+
try {
90+
await SafePromiseRace([
91+
callMethod('NodeRuntime.enable'),
92+
closedPromise,
93+
]);
94+
await SafePromiseRace([
95+
waitingPromise,
96+
closedPromise,
97+
]);
98+
} finally {
99+
client.removeListener('NodeRuntime.waitingForDebugger', onWaiting);
100+
client.removeListener('close', onClose);
101+
}
102+
}
103+
64104
function writeInspectUsageAndExit(invokedAs, message, exitCode) {
65105
const code = exitCode ?? (message ? kInvalidCommandLineArgument : 0);
66106
const out = code === 0 ? process.stdout : process.stderr;
@@ -189,5 +229,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
189229
module.exports = {
190230
ensureTrailingNewline,
191231
launchChildProcess,
232+
waitForDebugger,
192233
writeInspectUsageAndExit,
193234
};

‎lib/internal/debugger/inspect_probe.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client');
3333
const {
3434
ensureTrailingNewline,
3535
launchChildProcess,
36+
waitForDebugger,
3637
} = require('internal/debugger/inspect_helpers');
3738

3839
const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes;
@@ -1044,11 +1045,16 @@ class ProbeInspectorSession {
10441045
this.connected = true;
10451046

10461047
try {
1048+
await waitForDebugger(
1049+
this.client,
1050+
(method) => this.callCdp(method),
1051+
);
10471052
await this.callCdp('Runtime.enable');
10481053
await this.callCdp('Debugger.enable');
10491054
await this.bindBreakpoints();
10501055
this.started = true;
10511056
this.startTimeout();
1057+
await this.callCdp('NodeRuntime.disable');
10521058
await this.callCdp('Runtime.runIfWaitingForDebugger');
10531059
} catch (err) {
10541060
if (err !== kInspectorFailedSentinel) { throw err; }

‎lib/internal/debugger/inspect_repl.js‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url');
6060
const { customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace } = require('internal/util');
6161
const { inspect: utilInspect } = require('internal/util/inspect');
6262
const { isObjectLiteral } = require('internal/repl/utils');
63+
const { waitForDebugger } = require('internal/debugger/inspect_helpers');
6364
const debuglog = require('internal/util/debuglog').debuglog('inspect');
6465

6566
const SHORTCUTS = {
@@ -1204,9 +1205,13 @@ function createRepl(inspector) {
12041205
}
12051206

12061207
async function initAfterStart() {
1208+
const waitForDebuggerOnStart = !!inspector.options?.script;
12071209
waitForInitialBreakRender =
1208-
!!inspector.options?.script &&
1210+
waitForDebuggerOnStart &&
12091211
process.env.NODE_INSPECT_RESUME_ON_START !== '1';
1212+
if (waitForDebuggerOnStart) {
1213+
await waitForDebugger(inspector.client);
1214+
}
12101215
await Runtime.enable();
12111216
await Profiler.enable();
12121217
await Profiler.setSamplingInterval({ interval: 100 });
@@ -1215,6 +1220,9 @@ function createRepl(inspector) {
12151220
await Debugger.setBlackboxPatterns({ patterns: [] });
12161221
await Debugger.setPauseOnExceptions({ state: pauseOnExceptionState });
12171222
await restoreBreakpoints();
1223+
if (waitForDebuggerOnStart) {
1224+
await inspector.client.callMethod('NodeRuntime.disable');
1225+
}
12181226
await Runtime.runIfWaitingForDebugger();
12191227
await PromiseResolve();
12201228
waitForInitialBreakRender = false;

‎test/parallel/test-debugger-run-restart-init.js‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,27 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) {
7979
const runGate = createGate();
8080
const restartGate = createGate();
8181
const gates = [null, runGate, restartGate];
82+
const client = new EventEmitter();
83+
let nodeRuntimeEnableCount = 0;
84+
client.callMethod = async (method) => {
85+
calls.push(method);
86+
if (method === 'NodeRuntime.enable') {
87+
const emitWaiting = () => {
88+
calls.push('NodeRuntime.waitingForDebugger');
89+
client.emit('NodeRuntime.waitingForDebugger');
90+
};
91+
// Cover notifications arriving both before and after the enable reply.
92+
if (nodeRuntimeEnableCount++ % 2 === 0) {
93+
emitWaiting();
94+
} else {
95+
setImmediate(emitWaiting);
96+
}
97+
}
98+
};
8299
const inspector = {
83-
client: new EventEmitter(),
100+
client,
84101
domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'],
102+
options: { script: 'debugger-target.js' },
85103
stdin: new PassThrough(),
86104
stdout: new PassThrough(),
87105
run: common.mustCall(async () => {
@@ -101,6 +119,29 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) {
101119
await assertCommandWaitsForInit(repl, 'run', runGate, calls);
102120
await assertCommandWaitsForInit(repl, 'restart', restartGate, calls);
103121

122+
assert.deepStrictEqual(
123+
calls.filter((call) => (
124+
call === 'NodeRuntime.enable' ||
125+
call === 'NodeRuntime.waitingForDebugger' ||
126+
call === 'NodeRuntime.disable' ||
127+
call === 'Runtime.runIfWaitingForDebugger'
128+
)),
129+
[
130+
'NodeRuntime.enable',
131+
'NodeRuntime.waitingForDebugger',
132+
'NodeRuntime.disable',
133+
'Runtime.runIfWaitingForDebugger',
134+
'NodeRuntime.enable',
135+
'NodeRuntime.waitingForDebugger',
136+
'NodeRuntime.disable',
137+
'Runtime.runIfWaitingForDebugger',
138+
'NodeRuntime.enable',
139+
'NodeRuntime.waitingForDebugger',
140+
'NodeRuntime.disable',
141+
'Runtime.runIfWaitingForDebugger',
142+
],
143+
);
144+
104145
assert.deepStrictEqual(
105146
calls.filter((call) => (
106147
call === 'inspector.run' ||

0 commit comments

Comments
 (0)