Skip to content

Commit 78bbee3

Browse files
authored
debugger: add --help to node inspect and improve docs
- Add `--help` / `-h` to `node inspect` covering both interactive and non-interactive probe modes. The help text is printed when `--help`/`-h` appears before any positional argument to avoid hijacking `--help` passed to a child script. - Improve the documentation of probe mode and add examples, explain same-location probe coalescing, TDZ caveat for let/const bindings, basename matching and exit code behavior. Also move it to a section parallel to interactive mode. Remove recommendation of evaluating structured expressions as that is prone to missing info in JSON mode. Drive-by: When probe mode exits due to invalid arguments, exit with `kInvalidCommandLineArgument` (9) instead of `kGenericUserError` (1). Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: #63201 Reviewed-By: Jan Martin <jan.krems@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Aviv Keller <me@aviv.sh>
1 parent a2aeb72 commit 78bbee3

8 files changed

Lines changed: 460 additions & 217 deletions

doc/api/debugger.md

Lines changed: 253 additions & 134 deletions
Large diffs are not rendered by default.

lib/internal/debugger/inspect.js

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ const {
2727
const InspectClient = require('internal/debugger/inspect_client');
2828
const {
2929
launchChildProcess,
30-
writeUsageAndExit,
30+
writeInspectUsageAndExit,
3131
} = require('internal/debugger/inspect_helpers');
3232
const {
33-
startProbeMode,
33+
parseProbeTokens,
34+
runProbeMode,
3435
} = require('internal/debugger/inspect_probe');
3536
const createRepl = require('internal/debugger/inspect_repl');
3637

@@ -39,6 +40,7 @@ const debuglog = util.debuglog('inspect');
3940
const {
4041
exitCodes: {
4142
kGenericUserError,
43+
kInvalidCommandLineArgument,
4244
kNoFailure,
4345
},
4446
} = internalBinding('errors');
@@ -220,7 +222,7 @@ class NodeInspector {
220222
}
221223
}
222224

223-
function parseArgv(args) {
225+
function parseInteractiveArgs(args) {
224226
const target = ArrayPrototypeShift(args);
225227
let host = '127.0.0.1';
226228
let port = 9229;
@@ -264,20 +266,83 @@ function parseArgv(args) {
264266
};
265267
}
266268

269+
const kInspectArgOptions = {
270+
__proto__: null,
271+
expr: { type: 'string' },
272+
help: { type: 'boolean', short: 'h' },
273+
json: { type: 'boolean' },
274+
// Port and timeout use type 'string' because parseArgs has no
275+
// numeric type; the values are parsed to integers by parseProbeTokens().
276+
port: { type: 'string' },
277+
preview: { type: 'boolean' },
278+
probe: { type: 'string' },
279+
timeout: { type: 'string' },
280+
};
281+
282+
// Parses args once and decides whether the user wants the inspect help, probe
283+
// mode, or interactive mode. The mode is determined by the first option,
284+
// option-terminator, or positional token in the input.
285+
//
286+
// Returns one of:
287+
// { mode: 'help' }
288+
// { mode: 'probe', tokens, args }
289+
// { mode: 'interactive' }
290+
function parseInspectMode(args) {
291+
const { tokens } = util.parseArgs({
292+
args,
293+
allowPositionals: true,
294+
options: kInspectArgOptions,
295+
strict: false,
296+
tokens: true,
297+
});
298+
299+
for (const token of tokens) {
300+
if (token.kind === 'option') {
301+
if (token.name === 'help') return { mode: 'help' };
302+
if (token.name === 'probe') {
303+
// `--probe --help` / `--probe -h` (no value) consumes the help flag
304+
// as the probe's "value"; surface help instead of a probe error.
305+
if (!token.inlineValue &&
306+
(token.value === '--help' || token.value === '-h')) {
307+
return { mode: 'help' };
308+
}
309+
return { mode: 'probe', tokens, args };
310+
}
311+
}
312+
if (token.kind === 'option-terminator' || token.kind === 'positional') {
313+
break;
314+
}
315+
}
316+
return { mode: 'interactive' };
317+
}
318+
267319
function startInspect(argv = ArrayPrototypeSlice(process.argv, 2),
268320
stdin = process.stdin,
269321
stdout = process.stdout) {
270322
const invokedAs = `${process.argv0} ${process.argv[1]}`;
271323

272324
if (argv.length < 1) {
273-
writeUsageAndExit(invokedAs);
325+
writeInspectUsageAndExit(invokedAs, undefined, kInvalidCommandLineArgument);
274326
}
275327

276-
if (startProbeMode(invokedAs, argv, stdout)) {
328+
const parsed = parseInspectMode(argv);
329+
330+
if (parsed.mode === 'help') {
331+
writeInspectUsageAndExit(invokedAs);
332+
}
333+
334+
if (parsed.mode === 'probe') {
335+
let probeOptions;
336+
try {
337+
probeOptions = parseProbeTokens(parsed.tokens, parsed.args);
338+
} catch (error) {
339+
writeInspectUsageAndExit(invokedAs, error.message, kInvalidCommandLineArgument);
340+
}
341+
runProbeMode(stdout, probeOptions);
277342
return;
278343
}
279344

280-
const options = parseArgv(argv);
345+
const options = parseInteractiveArgs(argv);
281346
const inspector = new NodeInspector(options, stdin, stdout);
282347

283348
stdin.resume();

lib/internal/debugger/inspect_helpers.js

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,67 @@ function ensureTrailingNewline(text) {
6161
return StringPrototypeEndsWith(text, '\n') ? text : `${text}\n`;
6262
}
6363

64-
function writeUsageAndExit(invokedAs, message, exitCode = kInvalidCommandLineArgument) {
64+
function writeInspectUsageAndExit(invokedAs, message, exitCode) {
65+
const code = exitCode ?? (message ? kInvalidCommandLineArgument : 0);
66+
const out = code === 0 ? process.stdout : process.stderr;
6567
if (message) {
66-
process.stderr.write(`${message}\n`);
68+
out.write(`${message}\n`);
6769
}
68-
const text =
69-
`Usage: ${invokedAs} script.js\n` +
70-
` ${invokedAs} <host>:<port>\n` +
71-
` ${invokedAs} --port=<port> Use 0 for random port assignment\n` +
72-
` ${invokedAs} -p <pid>\n` +
73-
` ${invokedAs} [--json] [--timeout=<ms>] [--port=<port>] ` +
74-
`--probe <file>:<line>[:<col>] --expr <expr> ` +
75-
`[--probe <file>:<line>[:<col>] --expr <expr> ...] ` +
76-
`[--] [<node-option> ...] <script.js> [args...]\n`;
77-
process.stderr.write(text);
78-
process.exit(exitCode);
70+
out.write(`Usage: ${invokedAs} [--port=<port>] [<node-option> ...]
71+
[<script> [<script-args>] | <host>:<port> | -p <pid>]
72+
${invokedAs} --probe <file>:<line>[:<col>] --expr <expr>
73+
[--probe <file>:<line>[:<col>] --expr <expr> ...]
74+
[--json] [--preview] [--timeout=<ms>] [--port=<port>]
75+
[--] [<node-option> ...] <script> [<script-args> ...]
76+
77+
Interactive mode: Starts a live debugging session.
78+
79+
Example:
80+
$ node inspect script.js
81+
82+
Options:
83+
--port=<port> Inspector port for the debuggee (default: 9229)
84+
<script> The script to launch and debug.
85+
<host>:<port> Remote debugger to connect to.
86+
-p <pid> Attach to a running Node.js process by PID
87+
88+
Semantics:
89+
* If neither a script nor a host:port nor -p is provided, node inspect starts
90+
the REPL.
91+
92+
Non-interactive probe mode: Evaluates expressions whenever execution reaches
93+
specified source locations and prints all the evaluation results to stdout.
94+
95+
Example:
96+
$ node inspect --probe app.js:10 --expr "user"
97+
--probe src/utils.js:5:15 --expr "config.options"
98+
--json --preview -- --no-warnings app.js --arg-for-app=foo
99+
100+
Options:
101+
--probe <file>:<line>[:<col>]
102+
Source location of the probe (1-based, col defaults
103+
to 1). Matches by file basename, use a fuller path to
104+
disambiguate. Must be immediately followed by --expr.
105+
--expr <expr> Expression to evaluate in the lexical scope of the
106+
preceding --probe each time execution reaches it.
107+
Avoid probing let/const-bound variables at their
108+
declaration site or a ReferenceError may be thrown.
109+
--json Output JSON if specified, otherwise human-readable text.
110+
--preview Include V8 object previews in JSON output.
111+
--timeout <ms> Global session timeout (default: 30000).
112+
--port <port> Inspector port for the debuggee (default: 0 = random).
113+
114+
Semantics:
115+
* Multiple --probe/--expr pairs are allowed. Same-location --probes share
116+
a pause and scope, their --exprs are evaluated in command-line order.
117+
* Use -- before any Node.js flags intended for the child process.
118+
* Target errors are surfaced in the report as a terminal 'error' event.
119+
The probing process exits 0 unless it encounters an error itself.
120+
121+
See https://nodejs.org/api/debugger.html for details, including the
122+
probe output schema.
123+
`);
124+
process.exit(code);
79125
}
80126

81127
async function launchChildProcess(childArgs, inspectHost, inspectPort,
@@ -128,5 +174,5 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
128174
module.exports = {
129175
ensureTrailingNewline,
130176
launchChildProcess,
131-
writeUsageAndExit,
177+
writeInspectUsageAndExit,
132178
};

lib/internal/debugger/inspect_probe.js

Lines changed: 3 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,12 @@ const {
2323
} = primordials;
2424

2525
const { clearTimeout, setTimeout } = require('timers');
26-
const util = require('util');
2726
const { SideEffectFreeRegExpPrototypeSymbolReplace } = require('internal/util');
2827

2928
const InspectClient = require('internal/debugger/inspect_client');
3029
const {
3130
ensureTrailingNewline,
3231
launchChildProcess,
33-
writeUsageAndExit,
3432
} = require('internal/debugger/inspect_helpers');
3533

3634
const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes;
@@ -46,17 +44,6 @@ const kProbeVersion = 1;
4644
const kProbeDisconnectSentinel = 'Waiting for the debugger to disconnect...';
4745
const kDigitsRegex = /^\d+$/;
4846
const kInspectPortRegex = /^--inspect-port=(\d+)$/;
49-
const kProbeArgOptions = {
50-
__proto__: null,
51-
expr: { type: 'string' },
52-
json: { type: 'boolean' },
53-
// Port and timeout use type 'string' because parseArgs has no
54-
// numeric type; the values are parsed to integers in parseProbeArgv().
55-
port: { type: 'string' },
56-
preview: { type: 'boolean' },
57-
probe: { type: 'string' },
58-
timeout: { type: 'string' },
59-
};
6047

6148
function parseUnsignedInteger(value, name, allowZero = false) {
6249
if (typeof value !== 'string' || RegExpPrototypeExec(kDigitsRegex, value) === null) {
@@ -274,29 +261,7 @@ function buildProbeTextReport(report) {
274261
return ensureTrailingNewline(ArrayPrototypeJoin(lines, '\n'));
275262
}
276263

277-
function hasTopLevelProbeOption(args) {
278-
const { tokens } = util.parseArgs({
279-
args,
280-
allowPositionals: true,
281-
options: kProbeArgOptions,
282-
strict: false,
283-
tokens: true,
284-
});
285-
286-
for (const token of tokens) {
287-
if (token.kind === 'option' && token.name === 'probe') {
288-
return true;
289-
}
290-
291-
if (token.kind === 'option-terminator' || token.kind === 'positional') {
292-
return false;
293-
}
294-
}
295-
296-
return false;
297-
}
298-
299-
function parseProbeArgv(args) {
264+
function parseProbeTokens(tokens, args) {
300265
let port = 0;
301266
let preview = false;
302267
let timeout = kProbeDefaultTimeout;
@@ -307,14 +272,6 @@ function parseProbeArgv(args) {
307272
let expectedExprIndex = -1;
308273
const probes = [];
309274

310-
const { tokens } = util.parseArgs({
311-
args,
312-
allowPositionals: true,
313-
options: kProbeArgOptions,
314-
strict: false,
315-
tokens: true,
316-
});
317-
318275
for (const token of tokens) {
319276
if (token.kind === 'option-terminator') {
320277
sawSeparator = true;
@@ -764,22 +721,7 @@ async function runProbeMode(stdout, probeOptions) {
764721
}
765722
}
766723

767-
function startProbeMode(invokedAs, args, stdout) {
768-
if (!hasTopLevelProbeOption(args)) {
769-
return false;
770-
}
771-
772-
let probeOptions;
773-
try {
774-
probeOptions = parseProbeArgv(args);
775-
} catch (error) {
776-
writeUsageAndExit(invokedAs, error.message, kGenericUserError);
777-
}
778-
779-
runProbeMode(stdout, probeOptions);
780-
return true;
781-
}
782-
783724
module.exports = {
784-
startProbeMode,
725+
parseProbeTokens,
726+
runProbeMode,
785727
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Tests that --help / -h are forwarded to the debuggee when a script is
2+
// also passed, instead of being hijacked as the inspect help flag.
3+
4+
import { skipIfInspectorDisabled } from '../common/index.mjs';
5+
6+
skipIfInspectorDisabled();
7+
8+
import * as fixtures from '../common/fixtures.mjs';
9+
import startCLI from '../common/debugger.js';
10+
11+
import assert from 'assert';
12+
13+
async function getEvaluatedArgv(flag) {
14+
const script = fixtures.path('debugger', 'empty.js');
15+
const cli = startCLI([script, flag]);
16+
try {
17+
await cli.waitForInitialBreak();
18+
await cli.waitForPrompt();
19+
await cli.command('exec process.argv');
20+
return cli.output;
21+
} finally {
22+
await cli.quit();
23+
}
24+
}
25+
26+
async function checkForwardedHelp(flag) {
27+
const output = await getEvaluatedArgv(flag);
28+
assert(output.includes(`'${flag}'`),
29+
`expected debuggee process.argv to include "${flag}", got:\n${output}`);
30+
assert.doesNotMatch(output, /Usage: .+ inspect/);
31+
}
32+
33+
await checkForwardedHelp('--help');
34+
await checkForwardedHelp('-h');
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
// Tests that `node inspect` help text is printed when
4+
// `--help` / `-h` is passed and there is no script,
5+
// or when there are no additional arguments.
6+
const common = require('../common');
7+
common.skipIfInspectorDisabled();
8+
9+
const {
10+
spawnSyncAndAssert,
11+
spawnSyncAndExit,
12+
} = require('../common/child_process');
13+
14+
const usageRegex = /^Usage: .+ inspect .*debugger\.html/ms;
15+
16+
// --help / -h prints the usage to stdout and exits with code 0,
17+
// for both interactive and probe-mode invocations.
18+
const helpArgs = [
19+
['inspect', '--help'],
20+
['inspect', '-h'],
21+
['inspect', '--probe', '--help'],
22+
['inspect', '--probe', '-h'],
23+
];
24+
25+
for (const args of helpArgs) {
26+
spawnSyncAndAssert(process.execPath, args, {
27+
stdout: usageRegex,
28+
});
29+
}
30+
31+
// `node inspect` with no args prints the usage to stderr and exits
32+
// with kInvalidCommandLineArgument (9).
33+
spawnSyncAndExit(process.execPath, ['inspect'], {
34+
status: 9,
35+
signal: null,
36+
stderr: usageRegex,
37+
});

test/parallel/test-debugger-probe-missing-expr.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ spawnSyncAndExit(process.execPath, [
1313
probeScript,
1414
], {
1515
signal: null,
16-
status: 1,
16+
status: 9,
1717
stderr: /Each --probe must be followed immediately by --expr/,
1818
trim: true,
1919
});

0 commit comments

Comments
 (0)