nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
377 lines (332 loc) • 10.1 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/debugger/inspect.js
import * as __hoisted_internal_debugger_inspect_repl__ from "nstdlib/lib/internal/debugger/inspect_repl";
import { spawn } from "nstdlib/lib/child_process";
import { EventEmitter } from "nstdlib/lib/events";
import * as net from "nstdlib/lib/net";
import * as util from "nstdlib/lib/util";
import {
setInterval as pSetInterval,
setTimeout as pSetTimeout,
} from "nstdlib/lib/timers/promises";
import { AbortController } from "nstdlib/lib/internal/abort_controller";
import { codes as __codes__ } from "nstdlib/lib/internal/errors";
import { exitCodes as __exitCodes__ } from "nstdlib/stub/binding/errors";
import * as __hoisted_internal_debugger_inspect_client__ from "nstdlib/lib/internal/debugger/inspect_client";
const { 0: InspectClient, 1: createRepl } = [
__hoisted_internal_debugger_inspect_client__,
__hoisted_internal_debugger_inspect_repl__,
];
const debuglog = util.debuglog("inspect");
const { ERR_DEBUGGER_STARTUP_ERROR } = __codes__;
const { kGenericUserError, kNoFailure, kInvalidCommandLineArgument } =
__exitCodes__;
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 = [`--inspect-brk=${inspectPort}`, script];
Array.prototype.push.apply(args, 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 = RegExp.prototype.exec.call(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 Promise.prototype.then.call(
Promise.resolve(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 = Function.prototype.bind.call(
runScript,
null,
options.script,
options.scriptArgs,
options.host,
options.port,
Function.prototype.bind.call(this.childPrint, this),
);
} else {
this._runScript = () =>
Promise.resolve([null, options.port, options.host]);
}
this.client = new InspectClient();
this.domainNames = ["Debugger", "HeapProfiler", "Profiler", "Runtime"];
Array.prototype.forEach.call(this.domainNames, (domain) => {
this[domain] = createAgentProxy(domain, this.client);
});
this.handleDebugEvent = (fullName, params) => {
const { 0: domain, 1: name } = String.prototype.split.call(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 = RegExp.prototype[Symbol.split].call(
/\r\n|\r|\n/g,
this.#stdioBuffers[which] + text,
);
this.#stdioBuffers[which] = "";
if (lines[lines.length - 1] !== "") {
this.#stdioBuffers[which] = Array.prototype.pop.call(lines);
}
const textToPrint = Array.prototype.join.call(
Array.prototype.map.call(lines, (chunk) => `< ${chunk}`),
"\n",
);
if (lines.length) {
this.print(textToPrint, true);
if (!this.paused) {
this.repl.displayPrompt(true);
}
}
if (
String.prototype.endsWith.call(
textToPrint,
"Waiting for the debugger to disconnect...\n",
)
) {
this.killChild();
}
}
}
function parseArgv(args) {
const target = Array.prototype.shift.call(args);
let host = "127.0.0.1";
let port = 9229;
let isRemote = false;
let script = target;
let scriptArgs = args;
const hostMatch = RegExp.prototype.exec.call(/^([^:]+):(\d+)$/, target);
const portMatch = RegExp.prototype.exec.call(/^--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 = Array.prototype.slice.call(args, 1);
} else if (
args.length === 1 &&
RegExp.prototype.exec.call(/^\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") {
process.stderr.write(`Target process: ${pid} doesn't exist.\n`);
process.exit(kGenericUserError);
}
throw e;
}
script = null;
isRemote = true;
}
return {
host,
port,
isRemote,
script,
scriptArgs,
};
}
function startInspect(
argv = Array.prototype.slice.call(process.argv, 2),
stdin = process.stdin,
stdout = process.stdout,
) {
if (argv.length < 1) {
const invokedAs = `${process.argv0} ${process.argv[1]}`;
process.stderr.write(
`Usage: ${invokedAs} script.js\n` +
` ${invokedAs} <host>:<port>\n` +
` ${invokedAs} --port=<port> Use 0 for random port assignment\n` +
` ${invokedAs} -p <pid>\n`,
);
process.exit(kInvalidCommandLineArgument);
}
const options = parseArgv(argv);
const inspector = new NodeInspector(options, stdin, stdout);
stdin.resume();
function handleUnexpectedError(e) {
if (e.code !== "ERR_DEBUGGER_STARTUP_ERROR") {
process.stderr.write(
"There was an internal error in Node.js. " +
"Please report this bug.\n" +
`${e.message}\n${e.stack}\n`,
);
} else {
process.stderr.write(e.message);
process.stderr.write("\n");
}
if (inspector.child) inspector.child.kill();
process.exit(kGenericUserError);
}
process.on("uncaughtException", handleUnexpectedError);
}
export { startInspect as start };