node/test/common/debugger.js
Joyee Cheung 9495906f8b
debugger: fix event listener leak in the run command
It should remove both the error and the ready event listeners
attached when either of them fires, instead of removing only
the one whose corresponding event fires, otherwise the other
event listener will always get leaked.

PR-URL: https://github.com/nodejs/node/pull/60464
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
2025-11-07 13:28:03 +00:00

191 lines
4.8 KiB
JavaScript

'use strict';
const common = require('../common');
const spawn = require('child_process').spawn;
const BREAK_MESSAGE = new RegExp('(?:' + [
'assert', 'break', 'break on start', 'debugCommand',
'exception', 'other', 'promiseRejection', 'step',
].join('|') + ') in', 'i');
let TIMEOUT = common.platformTimeout(10000);
// Some macOS and Windows machines require more time to receive the outputs from the client.
// https://github.com/nodejs/build/issues/3014
if (common.isWindows || common.isMacOS) {
TIMEOUT = common.platformTimeout(15000);
}
function isPreBreak(output) {
return /Break on start/.test(output) && /1 \(function \(exports/.test(output);
}
function startCLI(args, flags = [], spawnOpts = {}, opts = { randomPort: true }) {
let stderrOutput = '';
const child = spawn(process.execPath, [
...flags,
'inspect',
...(opts.randomPort !== false ? ['--port=0'] : []),
...args,
], spawnOpts);
const outputBuffer = [];
function bufferOutput(chunk) {
if (this === child.stderr) {
stderrOutput += chunk;
}
outputBuffer.push(chunk);
}
function getOutput() {
return outputBuffer.join('\n').replaceAll('\b', '');
}
child.stdout.setEncoding('utf8');
child.stdout.on('data', bufferOutput);
child.stderr.setEncoding('utf8');
child.stderr.on('data', bufferOutput);
if (process.env.VERBOSE === '1') {
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
}
return {
flushOutput() {
const output = this.output;
outputBuffer.length = 0;
return output;
},
waitFor(pattern) {
function checkPattern(str) {
if (Array.isArray(pattern)) {
return pattern.every((p) => p.test(str));
}
return pattern.test(str);
}
return new Promise((resolve, reject) => {
function checkOutput() {
if (checkPattern(getOutput())) {
tearDown();
resolve();
}
}
function onChildClose(code, signal) {
tearDown();
let message = 'Child exited';
if (code) {
message += `, code ${code}`;
}
if (signal) {
message += `, signal ${signal}`;
}
message += ` while waiting for ${pattern}; found: ${this.output}`;
if (stderrOutput) {
message += `\n STDERR: ${stderrOutput}`;
}
reject(new Error(message));
}
// Capture stack trace here to show where waitFor was called from when it times out.
const timeoutErr = new Error(`Timeout (${TIMEOUT}) while waiting for ${pattern}`);
const timer = setTimeout(() => {
tearDown();
timeoutErr.output = this.output;
reject(timeoutErr);
}, TIMEOUT);
function tearDown() {
clearTimeout(timer);
child.stdout.removeListener('data', checkOutput);
child.removeListener('close', onChildClose);
}
child.on('close', onChildClose);
child.stdout.on('data', checkOutput);
checkOutput();
});
},
waitForPrompt() {
return this.waitFor(/>\s+$/);
},
async waitForInitialBreak() {
await this.waitFor(/break (?:on start )?in/i);
if (isPreBreak(this.output)) {
await this.command('next', false);
return this.waitFor(/break in/);
}
},
get breakInfo() {
const output = this.output;
const breakMatch =
output.match(/(step |break (?:on start )?)in ([^\n]+):(\d+)\n/i);
if (breakMatch === null) {
throw new Error(
`Could not find breakpoint info in ${JSON.stringify(output)}`);
}
return { filename: breakMatch[2], line: +breakMatch[3] };
},
ctrlC() {
return this.command('.interrupt');
},
get output() {
return getOutput();
},
get stderrOutput() {
return stderrOutput;
},
get rawOutput() {
return outputBuffer.join('').toString();
},
parseSourceLines() {
return getOutput().split('\n')
.map((line) => line.match(/(?:\*|>)?\s*(\d+)/))
.filter((match) => match !== null)
.map((match) => +match[1]);
},
writeLine(input, flush = true) {
if (flush) {
this.flushOutput();
}
if (process.env.VERBOSE === '1') {
process.stderr.write(`< ${input}\n`);
}
child.stdin.write(input);
child.stdin.write('\n');
},
command(input, flush = true) {
this.writeLine(input, flush);
return this.waitForPrompt();
},
stepCommand(input) {
this.writeLine(input, true);
return this
.waitFor(BREAK_MESSAGE)
.then(() => this.waitForPrompt());
},
quit() {
return new Promise((resolve) => {
child.stdin.end();
child.on('close', resolve);
});
},
};
}
module.exports = startCLI;