mirror of
https://github.com/nodejs/node.git
synced 2026-10-29 02:35:20 +08:00
On a hot path, the probe can record every hit and require
filtering afterwards. This patch adds a per-probe `--cond <expr>`
option that allows limiting the hit to only when the expression
is truthy at the probe location. V8 evaluates it as the breakpoint's
native condition, so the target is not paused when it does not hold,
and a condition that throws is treated as false. Since in CDP,
a location can only carry one breakpoint per URL pattern,
probes sharing a location must share one condition (or none).
Conflicting conditions are rejected.
Example:
```js
// app.js
let total = 0;
for (let i = 0; i < 10; i++) {
total += i; // line 4
}
```
```
$ out/Release/node inspect --probe app.js:4 --expr 'total' \
--cond 'i % 3 === 0' app.js
```
```
Hit 1 at file:///path/to/app.js:3:3
total = 0
Hit 2 at file:///path/to/app.js:3:3
total = 3
Hit 3 at file:///path/to/app.js:3:3
total = 15
Hit 4 at file:///path/to/app.js:3:3
total = 36
Completed
```
Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/64328
Refs: https://github.com/nodejs/node/issues/63646
Reviewed-By: James M Snell <jasnell@gmail.com>
368 lines
9.6 KiB
JavaScript
368 lines
9.6 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayPrototypeForEach,
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypeMap,
|
|
ArrayPrototypePop,
|
|
ArrayPrototypeShift,
|
|
ArrayPrototypeSlice,
|
|
FunctionPrototypeBind,
|
|
Number,
|
|
PromisePrototypeThen,
|
|
PromiseResolve,
|
|
Proxy,
|
|
RegExpPrototypeExec,
|
|
RegExpPrototypeSymbolSplit,
|
|
StringPrototypeEndsWith,
|
|
StringPrototypeSplit,
|
|
} = primordials;
|
|
|
|
const { EventEmitter } = require('events');
|
|
const util = require('util');
|
|
const {
|
|
setTimeout: pSetTimeout,
|
|
} = require('timers/promises');
|
|
|
|
const InspectClient = require('internal/debugger/inspect_client');
|
|
const {
|
|
launchChildProcess,
|
|
writeInspectUsageAndExit,
|
|
} = require('internal/debugger/inspect_helpers');
|
|
const {
|
|
parseProbeTokens,
|
|
runProbeMode,
|
|
} = require('internal/debugger/inspect_probe');
|
|
const createRepl = require('internal/debugger/inspect_repl');
|
|
|
|
const debuglog = util.debuglog('inspect');
|
|
|
|
const {
|
|
exitCodes: {
|
|
kGenericUserError,
|
|
kInvalidCommandLineArgument,
|
|
kNoFailure,
|
|
},
|
|
} = internalBinding('errors');
|
|
|
|
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(
|
|
launchChildProcess, 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, '.', 2);
|
|
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 parseInteractiveArgs(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') {
|
|
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,
|
|
};
|
|
}
|
|
|
|
const kInspectArgOptions = {
|
|
'__proto__': null,
|
|
'cond': { type: 'string' },
|
|
'expr': { type: 'string' },
|
|
'help': { type: 'boolean', short: 'h' },
|
|
'json': { type: 'boolean' },
|
|
// Port, timeout, and max-hit use type 'string' because parseArgs has no
|
|
// numeric type; the values are parsed to integers by parseProbeTokens().
|
|
'max-hit': { type: 'string' },
|
|
'port': { type: 'string' },
|
|
'preview': { type: 'boolean' },
|
|
'probe': { type: 'string' },
|
|
'timeout': { type: 'string' },
|
|
};
|
|
|
|
// Parses args once and decides whether the user wants the inspect help, probe
|
|
// mode, or interactive mode. The mode is determined by the first option,
|
|
// option-terminator, or positional token in the input.
|
|
//
|
|
// Returns one of:
|
|
// { mode: 'help' }
|
|
// { mode: 'probe', tokens, args }
|
|
// { mode: 'interactive' }
|
|
function parseInspectMode(args) {
|
|
const { tokens } = util.parseArgs({
|
|
args,
|
|
allowPositionals: true,
|
|
options: kInspectArgOptions,
|
|
strict: false,
|
|
tokens: true,
|
|
});
|
|
|
|
for (const token of tokens) {
|
|
if (token.kind === 'option') {
|
|
if (token.name === 'help') return { mode: 'help' };
|
|
if (token.name === 'probe') {
|
|
// `--probe --help` / `--probe -h` (no value) consumes the help flag
|
|
// as the probe's "value"; surface help instead of a probe error.
|
|
if (!token.inlineValue &&
|
|
(token.value === '--help' || token.value === '-h')) {
|
|
return { mode: 'help' };
|
|
}
|
|
return { mode: 'probe', tokens, args };
|
|
}
|
|
}
|
|
if (token.kind === 'option-terminator' || token.kind === 'positional') {
|
|
break;
|
|
}
|
|
}
|
|
return { mode: 'interactive' };
|
|
}
|
|
|
|
function startInspect(argv = ArrayPrototypeSlice(process.argv, 2),
|
|
stdin = process.stdin,
|
|
stdout = process.stdout) {
|
|
const invokedAs = `${process.argv0} ${process.argv[1]}`;
|
|
|
|
if (argv.length < 1) {
|
|
writeInspectUsageAndExit(invokedAs, undefined, kInvalidCommandLineArgument);
|
|
}
|
|
|
|
const parsed = parseInspectMode(argv);
|
|
|
|
if (parsed.mode === 'help') {
|
|
writeInspectUsageAndExit(invokedAs);
|
|
}
|
|
|
|
if (parsed.mode === 'probe') {
|
|
let probeOptions;
|
|
try {
|
|
probeOptions = parseProbeTokens(parsed.tokens, parsed.args);
|
|
} catch (error) {
|
|
writeInspectUsageAndExit(invokedAs, error.message, kInvalidCommandLineArgument);
|
|
}
|
|
runProbeMode(stdout, probeOptions);
|
|
return;
|
|
}
|
|
|
|
const options = parseInteractiveArgs(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);
|
|
}
|
|
exports.start = startInspect;
|