Files
node/test/parallel/test-debugger-run-restart-init.js
Filip SkokanandArchkon 2004fbd164 debugger: wait for target startup
The inspector can accept a connection before an --inspect-brk target
enters its frontend wait. Runtime.runIfWaitingForDebugger can then be
handled too early, allowing the target to subsequently block forever.

Wait for NodeRuntime.waitingForDebugger before initializing and
releasing launched targets. Race the handshake against disconnects and
apply it to both interactive and probe startup.

Refs: https://github.com/nodejs/node/issues/64116
Assisted-by: codex:gpt-5.6-sol
Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com>
Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com>
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65194
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
2026-08-17 11:08:38 +03:00

184 lines
4.8 KiB
JavaScript

// Flags: --expose-internals
'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
const assert = require('assert');
const { EventEmitter } = require('events');
const { PassThrough } = require('stream');
const createRepl = require('internal/debugger/inspect_repl');
function createGate() {
let resolve;
const promise = new Promise((res) => {
resolve = res;
});
return { promise, resolve };
}
function createAgent(domain, calls, gates) {
const agent = new EventEmitter();
const method = (name) => async () => {
calls.push(`${domain}.${name}`);
if (domain === 'Runtime' && name === 'runIfWaitingForDebugger') {
const gate = gates.shift();
if (gate) {
await gate.promise;
}
}
};
agent.enable = method('enable');
agent.setSamplingInterval = method('setSamplingInterval');
agent.setAsyncCallStackDepth = method('setAsyncCallStackDepth');
agent.setBlackboxPatterns = method('setBlackboxPatterns');
agent.setPauseOnExceptions = method('setPauseOnExceptions');
agent.runIfWaitingForDebugger = method('runIfWaitingForDebugger');
return agent;
}
function evalCommand(repl, command) {
return new Promise((resolve, reject) => {
repl.eval(
command,
repl.context,
'debugger-repl-test',
(error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
},
);
});
}
async function assertCommandWaitsForInit(repl, command, gate, calls) {
let settled = false;
const promise = evalCommand(repl, command).then(() => {
settled = true;
});
await new Promise(setImmediate);
assert.strictEqual(
settled,
false,
`${command} resolved before post-connect initialization completed: ${calls}`,
);
gate.resolve();
await promise;
assert.strictEqual(settled, true);
}
(async () => {
const calls = [];
const runGate = createGate();
const restartGate = createGate();
const gates = [null, runGate, restartGate];
const client = new EventEmitter();
let nodeRuntimeEnableCount = 0;
client.callMethod = common.mustCall(async (method) => {
calls.push(method);
if (method === 'NodeRuntime.enable') {
const emitWaiting = () => {
calls.push('NodeRuntime.waitingForDebugger');
client.emit('NodeRuntime.waitingForDebugger');
};
// Cover notifications arriving both before and after the enable reply.
if (nodeRuntimeEnableCount++ % 2 === 0) {
emitWaiting();
} else {
setImmediate(emitWaiting);
}
} else {
assert.strictEqual(method, 'NodeRuntime.disable');
}
}, 6);
const inspector = {
client,
domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'],
options: { script: 'debugger-target.js' },
stdin: new PassThrough(),
stdout: new PassThrough(),
run: common.mustCall(async () => {
calls.push('inspector.run');
}, 2),
suspendReplWhile(fn) {
return fn();
},
};
for (const domain of inspector.domainNames) {
inspector[domain] = createAgent(domain, calls, gates);
}
const repl = await createRepl(inspector)();
await assertCommandWaitsForInit(repl, 'run', runGate, calls);
await assertCommandWaitsForInit(repl, 'restart', restartGate, calls);
assert.deepStrictEqual(
calls.filter((call) => (
call === 'NodeRuntime.enable' ||
call === 'NodeRuntime.waitingForDebugger' ||
call === 'NodeRuntime.disable' ||
call === 'Runtime.runIfWaitingForDebugger'
)),
[
'NodeRuntime.enable',
'NodeRuntime.waitingForDebugger',
'NodeRuntime.disable',
'Runtime.runIfWaitingForDebugger',
'NodeRuntime.enable',
'NodeRuntime.waitingForDebugger',
'NodeRuntime.disable',
'Runtime.runIfWaitingForDebugger',
'NodeRuntime.enable',
'NodeRuntime.waitingForDebugger',
'NodeRuntime.disable',
'Runtime.runIfWaitingForDebugger',
],
);
assert.deepStrictEqual(
calls.filter((call) => (
call === 'inspector.run' ||
call === 'Runtime.runIfWaitingForDebugger'
)),
[
'Runtime.runIfWaitingForDebugger',
'inspector.run',
'Runtime.runIfWaitingForDebugger',
'inspector.run',
'Runtime.runIfWaitingForDebugger',
],
);
repl.close();
const attachCalls = [];
const attachClient = new EventEmitter();
attachClient.callMethod = common.mustNotCall();
const attachInspector = {
client: attachClient,
domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'],
options: {},
stdin: new PassThrough(),
stdout: new PassThrough(),
suspendReplWhile(fn) {
return fn();
},
};
for (const domain of attachInspector.domainNames) {
attachInspector[domain] = createAgent(domain, attachCalls, []);
}
const attachRepl = await createRepl(attachInspector)();
attachRepl.close();
})().then(common.mustCall());