Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 141de35

Browse filesBrowse files
joyeecheungaduh95
authored andcommitted
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 d8475e1 commit 141de35
Copy full SHA for 141de35

8 files changed

+460-217Lines changed: 460 additions & 217 deletions
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎doc/api/debugger.md‎

Copy file name to clipboardExpand all lines: doc/api/debugger.md
+253-134Lines changed: 253 additions & 134 deletions
  • Display the source diff
  • Display the rich diff
Large diffs are not rendered by default.
Collapse file

‎lib/internal/debugger/inspect.js‎

Copy file name to clipboardExpand all lines: lib/internal/debugger/inspect.js
+71-6Lines 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();
Collapse file

‎lib/internal/debugger/inspect_helpers.js‎

Copy file name to clipboardExpand all lines: lib/internal/debugger/inspect_helpers.js
+60-14Lines 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
};
Collapse file

‎lib/internal/debugger/inspect_probe.js‎

Copy file name to clipboardExpand all lines: lib/internal/debugger/inspect_probe.js
+3-61Lines 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
};
Collapse file
+34Lines 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');
Collapse file
+37Lines 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+
});
Collapse file

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

Copy file name to clipboardExpand all lines: test/parallel/test-debugger-probe-missing-expr.js
+1-1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.