mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
Wait for the target to finish starting before attaching the debugger. Capture the CLI close event when it is spawned so quit() also completes after an early exit, and terminate the target before awaiting cleanup. Add a regression test for quitting an already exited CLI and remove the Windows flaky entry. Refs: https://github.com/nodejs/node/issues/63212 Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66320 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
191 lines
4.8 KiB
JavaScript
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 closed = new Promise((resolve) => child.once('close', resolve));
|
|
|
|
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() {
|
|
child.stdin.end();
|
|
return closed;
|
|
},
|
|
};
|
|
}
|
|
module.exports = startCLI;
|