| '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; |