mirror of
https://github.com/nodejs/node.git
synced 2025-05-09 05:41:13 +00:00

Add an ExitCode enum class and use it throughout the code base instead of hard-coding the exit codes everywhere. At the moment, the exit codes used in many places do not actually conform to what the documentation describes. With the new enums (which are also available to the JS land as constants in an internal binding) we could migrate to a more consistent usage of the codes, and eventually expose the constants to the user land when they are stable enough. PR-URL: https://github.com/nodejs/node/pull/44746 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Darshan Sen <raisinten@gmail.com>
371 lines
9.7 KiB
JavaScript
371 lines
9.7 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayPrototypeConcat,
|
|
ArrayPrototypeForEach,
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypeMap,
|
|
ArrayPrototypePop,
|
|
ArrayPrototypeShift,
|
|
ArrayPrototypeSlice,
|
|
FunctionPrototypeBind,
|
|
Number,
|
|
Promise,
|
|
PromisePrototypeThen,
|
|
PromiseResolve,
|
|
Proxy,
|
|
RegExpPrototypeExec,
|
|
RegExpPrototypeSymbolSplit,
|
|
StringPrototypeEndsWith,
|
|
StringPrototypeSplit,
|
|
} = primordials;
|
|
|
|
const { spawn } = require('child_process');
|
|
const { EventEmitter } = require('events');
|
|
const net = require('net');
|
|
const util = require('util');
|
|
const {
|
|
setInterval: pSetInterval,
|
|
setTimeout: pSetTimeout,
|
|
} = require('timers/promises');
|
|
const {
|
|
AbortController,
|
|
} = require('internal/abort_controller');
|
|
|
|
// TODO(aduh95): remove console calls
|
|
const console = require('internal/console/global');
|
|
|
|
const { 0: InspectClient, 1: createRepl } =
|
|
[
|
|
require('internal/debugger/inspect_client'),
|
|
require('internal/debugger/inspect_repl'),
|
|
];
|
|
|
|
const debuglog = util.debuglog('inspect');
|
|
|
|
const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes;
|
|
const {
|
|
exitCodes: {
|
|
kGenericUserError,
|
|
kNoFailure,
|
|
},
|
|
} = internalBinding('errors');
|
|
|
|
async function portIsFree(host, port, timeout = 3000) {
|
|
if (port === 0) return; // Binding to a random port.
|
|
|
|
const retryDelay = 150;
|
|
const ac = new AbortController();
|
|
const { signal } = ac;
|
|
|
|
pSetTimeout(timeout).then(() => ac.abort());
|
|
|
|
const asyncIterator = pSetInterval(retryDelay);
|
|
while (true) {
|
|
await asyncIterator.next();
|
|
if (signal.aborted) {
|
|
throw new ERR_DEBUGGER_STARTUP_ERROR(
|
|
`Timeout (${timeout}) waiting for ${host}:${port} to be free`);
|
|
}
|
|
const error = await new Promise((resolve) => {
|
|
const socket = net.connect(port, host);
|
|
socket.on('error', resolve);
|
|
socket.on('connect', () => {
|
|
socket.end();
|
|
resolve();
|
|
});
|
|
});
|
|
if (error?.code === 'ECONNREFUSED') {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const debugRegex = /Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\//;
|
|
async function runScript(script, scriptArgs, inspectHost, inspectPort,
|
|
childPrint) {
|
|
await portIsFree(inspectHost, inspectPort);
|
|
const args = ArrayPrototypeConcat(
|
|
[`--inspect-brk=${inspectPort}`, script],
|
|
scriptArgs);
|
|
const child = spawn(process.execPath, args);
|
|
child.stdout.setEncoding('utf8');
|
|
child.stderr.setEncoding('utf8');
|
|
child.stdout.on('data', (chunk) => childPrint(chunk, 'stdout'));
|
|
child.stderr.on('data', (chunk) => childPrint(chunk, 'stderr'));
|
|
|
|
let output = '';
|
|
return new Promise((resolve) => {
|
|
function waitForListenHint(text) {
|
|
output += text;
|
|
const debug = RegExpPrototypeExec(debugRegex, output);
|
|
if (debug) {
|
|
const host = debug[1];
|
|
const port = Number(debug[2]);
|
|
child.stderr.removeListener('data', waitForListenHint);
|
|
resolve([child, port, host]);
|
|
}
|
|
}
|
|
|
|
child.stderr.on('data', waitForListenHint);
|
|
});
|
|
}
|
|
|
|
function createAgentProxy(domain, client) {
|
|
const agent = new EventEmitter();
|
|
agent.then = (then, _catch) => {
|
|
// TODO: potentially fetch the protocol and pretty-print it here.
|
|
const descriptor = {
|
|
[util.inspect.custom](depth, { stylize }) {
|
|
return stylize(`[Agent ${domain}]`, 'special');
|
|
},
|
|
};
|
|
return PromisePrototypeThen(PromiseResolve(descriptor), then, _catch);
|
|
};
|
|
|
|
return new Proxy(agent, {
|
|
__proto__: null,
|
|
get(target, name) {
|
|
if (name in target) return target[name];
|
|
return function callVirtualMethod(params) {
|
|
return client.callMethod(`${domain}.${name}`, params);
|
|
};
|
|
},
|
|
});
|
|
}
|
|
|
|
class NodeInspector {
|
|
constructor(options, stdin, stdout) {
|
|
this.options = options;
|
|
this.stdin = stdin;
|
|
this.stdout = stdout;
|
|
|
|
this.paused = true;
|
|
this.child = null;
|
|
|
|
if (options.script) {
|
|
this._runScript = FunctionPrototypeBind(
|
|
runScript, null,
|
|
options.script,
|
|
options.scriptArgs,
|
|
options.host,
|
|
options.port,
|
|
FunctionPrototypeBind(this.childPrint, this));
|
|
} else {
|
|
this._runScript =
|
|
() => PromiseResolve([null, options.port, options.host]);
|
|
}
|
|
|
|
this.client = new InspectClient();
|
|
|
|
this.domainNames = ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'];
|
|
ArrayPrototypeForEach(this.domainNames, (domain) => {
|
|
this[domain] = createAgentProxy(domain, this.client);
|
|
});
|
|
this.handleDebugEvent = (fullName, params) => {
|
|
const { 0: domain, 1: name } = StringPrototypeSplit(fullName, '.');
|
|
if (domain in this) {
|
|
this[domain].emit(name, params);
|
|
}
|
|
};
|
|
this.client.on('debugEvent', this.handleDebugEvent);
|
|
const startRepl = createRepl(this);
|
|
|
|
// Handle all possible exits
|
|
process.on('exit', () => this.killChild());
|
|
const exitCodeZero = () => process.exit(kNoFailure);
|
|
process.once('SIGTERM', exitCodeZero);
|
|
process.once('SIGHUP', exitCodeZero);
|
|
|
|
(async () => {
|
|
try {
|
|
await this.run();
|
|
const repl = await startRepl();
|
|
this.repl = repl;
|
|
this.repl.on('exit', exitCodeZero);
|
|
this.paused = false;
|
|
} catch (error) {
|
|
process.nextTick(() => { throw error; });
|
|
}
|
|
})();
|
|
}
|
|
|
|
suspendReplWhile(fn) {
|
|
if (this.repl) {
|
|
this.repl.pause();
|
|
}
|
|
this.stdin.pause();
|
|
this.paused = true;
|
|
return (async () => {
|
|
try {
|
|
await fn();
|
|
this.paused = false;
|
|
if (this.repl) {
|
|
this.repl.resume();
|
|
this.repl.displayPrompt();
|
|
}
|
|
this.stdin.resume();
|
|
} catch (error) {
|
|
process.nextTick(() => { throw error; });
|
|
}
|
|
})();
|
|
}
|
|
|
|
killChild() {
|
|
this.client.reset();
|
|
if (this.child) {
|
|
this.child.kill();
|
|
this.child = null;
|
|
}
|
|
}
|
|
|
|
async run() {
|
|
this.killChild();
|
|
|
|
const { 0: child, 1: port, 2: host } = await this._runScript();
|
|
this.child = child;
|
|
|
|
this.print(`connecting to ${host}:${port} ..`, false);
|
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
debuglog('connection attempt #%d', attempt);
|
|
this.stdout.write('.');
|
|
try {
|
|
await this.client.connect(port, host);
|
|
debuglog('connection established');
|
|
this.stdout.write(' ok\n');
|
|
return;
|
|
} catch (error) {
|
|
debuglog('connect failed', error);
|
|
await pSetTimeout(1000);
|
|
}
|
|
}
|
|
this.stdout.write(' failed to connect, please retry\n');
|
|
process.exit(kGenericUserError);
|
|
}
|
|
|
|
clearLine() {
|
|
if (this.stdout.isTTY) {
|
|
this.stdout.cursorTo(0);
|
|
this.stdout.clearLine(1);
|
|
} else {
|
|
this.stdout.write('\b');
|
|
}
|
|
}
|
|
|
|
print(text, appendNewline = false) {
|
|
this.clearLine();
|
|
this.stdout.write(appendNewline ? `${text}\n` : text);
|
|
}
|
|
|
|
#stdioBuffers = { stdout: '', stderr: '' };
|
|
childPrint(text, which) {
|
|
const lines = RegExpPrototypeSymbolSplit(
|
|
/\r\n|\r|\n/g,
|
|
this.#stdioBuffers[which] + text);
|
|
|
|
this.#stdioBuffers[which] = '';
|
|
|
|
if (lines[lines.length - 1] !== '') {
|
|
this.#stdioBuffers[which] = ArrayPrototypePop(lines);
|
|
}
|
|
|
|
const textToPrint = ArrayPrototypeJoin(
|
|
ArrayPrototypeMap(lines, (chunk) => `< ${chunk}`),
|
|
'\n');
|
|
|
|
if (lines.length) {
|
|
this.print(textToPrint, true);
|
|
if (!this.paused) {
|
|
this.repl.displayPrompt(true);
|
|
}
|
|
}
|
|
|
|
if (StringPrototypeEndsWith(
|
|
textToPrint,
|
|
'Waiting for the debugger to disconnect...\n'
|
|
)) {
|
|
this.killChild();
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseArgv(args) {
|
|
const target = ArrayPrototypeShift(args);
|
|
let host = '127.0.0.1';
|
|
let port = 9229;
|
|
let isRemote = false;
|
|
let script = target;
|
|
let scriptArgs = args;
|
|
|
|
const hostMatch = RegExpPrototypeExec(/^([^:]+):(\d+)$/, target);
|
|
const portMatch = RegExpPrototypeExec(/^--port=(\d+)$/, target);
|
|
|
|
if (hostMatch) {
|
|
// Connecting to remote debugger
|
|
host = hostMatch[1];
|
|
port = Number(hostMatch[2]);
|
|
isRemote = true;
|
|
script = null;
|
|
} else if (portMatch) {
|
|
// Start on custom port
|
|
port = Number(portMatch[1]);
|
|
script = args[0];
|
|
scriptArgs = ArrayPrototypeSlice(args, 1);
|
|
} else if (args.length === 1 && RegExpPrototypeExec(/^\d+$/, args[0]) !== null &&
|
|
target === '-p') {
|
|
// Start debugger against a given pid
|
|
const pid = Number(args[0]);
|
|
try {
|
|
process._debugProcess(pid);
|
|
} catch (e) {
|
|
if (e.code === 'ESRCH') {
|
|
console.error(`Target process: ${pid} doesn't exist.`);
|
|
process.exit(kGenericUserError);
|
|
}
|
|
throw e;
|
|
}
|
|
script = null;
|
|
isRemote = true;
|
|
}
|
|
|
|
return {
|
|
host, port, isRemote, script, scriptArgs,
|
|
};
|
|
}
|
|
|
|
function startInspect(argv = ArrayPrototypeSlice(process.argv, 2),
|
|
stdin = process.stdin,
|
|
stdout = process.stdout) {
|
|
if (argv.length < 1) {
|
|
const invokedAs = `${process.argv0} ${process.argv[1]}`;
|
|
|
|
console.error(`Usage: ${invokedAs} script.js`);
|
|
console.error(` ${invokedAs} <host>:<port>`);
|
|
console.error(` ${invokedAs} --port=<port>`);
|
|
console.error(` ${invokedAs} -p <pid>`);
|
|
// TODO(joyeecheung): should be kInvalidCommandLineArgument.
|
|
process.exit(kGenericUserError);
|
|
}
|
|
|
|
const options = parseArgv(argv);
|
|
const inspector = new NodeInspector(options, stdin, stdout);
|
|
|
|
stdin.resume();
|
|
|
|
function handleUnexpectedError(e) {
|
|
if (e.code !== 'ERR_DEBUGGER_STARTUP_ERROR') {
|
|
console.error('There was an internal error in Node.js. ' +
|
|
'Please report this bug.');
|
|
console.error(e.message);
|
|
console.error(e.stack);
|
|
} else {
|
|
console.error(e.message);
|
|
}
|
|
if (inspector.child) inspector.child.kill();
|
|
process.exit(kGenericUserError);
|
|
}
|
|
|
|
process.on('uncaughtException', handleUnexpectedError);
|
|
}
|
|
exports.start = startInspect;
|