Skip to content

Commit bdc5713

Browse 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 5ea6c3e commit bdc5713

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
constInspectClient=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
constcreateRepl=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-
functionparseArgv(args){
225+
functionparseInteractiveArgs(args){
224226
consttarget=ArrayPrototypeShift(args);
225227
lethost='127.0.0.1';
226228
letport=9229;
@@ -264,20 +266,83 @@ function parseArgv(args) {
264266
};
265267
}
266268

269+
constkInspectArgOptions={
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+
functionparseInspectMode(args){
291+
const{ tokens }=util.parseArgs({
292+
args,
293+
allowPositionals: true,
294+
options: kInspectArgOptions,
295+
strict: false,
296+
tokens: true,
297+
});
298+
299+
for(consttokenoftokens){
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
functionstartInspect(argv=ArrayPrototypeSlice(process.argv,2),
268320
stdin=process.stdin,
269321
stdout=process.stdout){
270322
constinvokedAs=`${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+
constparsed=parseInspectMode(argv);
329+
330+
if(parsed.mode==='help'){
331+
writeInspectUsageAndExit(invokedAs);
332+
}
333+
334+
if(parsed.mode==='probe'){
335+
letprobeOptions;
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-
constoptions=parseArgv(argv);
345+
constoptions=parseInteractiveArgs(argv);
281346
constinspector=newNodeInspector(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
returnStringPrototypeEndsWith(text,'\n') ? text : `${text}\n`;
6262
}
6363

64-
functionwriteUsageAndExit(invokedAs,message,exitCode=kInvalidCommandLineArgument){
64+
functionwriteInspectUsageAndExit(invokedAs,message,exitCode){
65+
constcode=exitCode??(message ? kInvalidCommandLineArgument : 0);
66+
constout=code===0 ? process.stdout : process.stderr;
6567
if(message){
66-
process.stderr.write(`${message}\n`);
68+
out.write(`${message}\n`);
6769
}
68-
consttext=
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
asyncfunctionlaunchChildProcess(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-
constutil=require('util');
2726
const{ SideEffectFreeRegExpPrototypeSymbolReplace }=require('internal/util');
2827

2928
constInspectClient=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
constkProbeDisconnectSentinel='Waiting for the debugger to disconnect...';
4745
constkDigitsRegex=/^\d+$/;
4846
constkInspectPortRegex=/^--inspect-port=(\d+)$/;
49-
constkProbeArgOptions={
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
functionparseUnsignedInteger(value,name,allowZero=false){
6249
if(typeofvalue!=='string'||RegExpPrototypeExec(kDigitsRegex,value)===null){
@@ -274,29 +261,7 @@ function buildProbeTextReport(report) {
274261
returnensureTrailingNewline(ArrayPrototypeJoin(lines,'\n'));
275262
}
276263

277-
functionhasTopLevelProbeOption(args){
278-
const{ tokens }=util.parseArgs({
279-
args,
280-
allowPositionals: true,
281-
options: kProbeArgOptions,
282-
strict: false,
283-
tokens: true,
284-
});
285-
286-
for(consttokenoftokens){
287-
if(token.kind==='option'&&token.name==='probe'){
288-
returntrue;
289-
}
290-
291-
if(token.kind==='option-terminator'||token.kind==='positional'){
292-
returnfalse;
293-
}
294-
}
295-
296-
returnfalse;
297-
}
298-
299-
functionparseProbeArgv(args){
264+
functionparseProbeTokens(tokens,args){
300265
letport=0;
301266
letpreview=false;
302267
lettimeout=kProbeDefaultTimeout;
@@ -307,14 +272,6 @@ function parseProbeArgv(args) {
307272
letexpectedExprIndex=-1;
308273
constprobes=[];
309274

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

767-
functionstartProbeMode(invokedAs,args,stdout){
768-
if(!hasTopLevelProbeOption(args)){
769-
returnfalse;
770-
}
771-
772-
letprobeOptions;
773-
try{
774-
probeOptions=parseProbeArgv(args);
775-
}catch(error){
776-
writeUsageAndExit(invokedAs,error.message,kGenericUserError);
777-
}
778-
779-
runProbeMode(stdout,probeOptions);
780-
returntrue;
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*asfixturesfrom'../common/fixtures.mjs';
9+
importstartCLIfrom'../common/debugger.js';
10+
11+
importassertfrom'assert';
12+
13+
asyncfunctiongetEvaluatedArgv(flag){
14+
constscript=fixtures.path('debugger','empty.js');
15+
constcli=startCLI([script,flag]);
16+
try{
17+
awaitcli.waitForInitialBreak();
18+
awaitcli.waitForPrompt();
19+
awaitcli.command('exec process.argv');
20+
returncli.output;
21+
}finally{
22+
awaitcli.quit();
23+
}
24+
}
25+
26+
asyncfunctioncheckForwardedHelp(flag){
27+
constoutput=awaitgetEvaluatedArgv(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+
awaitcheckForwardedHelp('--help');
34+
awaitcheckForwardedHelp('-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+
constcommon=require('../common');
7+
common.skipIfInspectorDisabled();
8+
9+
const{
10+
spawnSyncAndAssert,
11+
spawnSyncAndExit,
12+
}=require('../common/child_process');
13+
14+
constusageRegex=/^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+
consthelpArgs=[
19+
['inspect','--help'],
20+
['inspect','-h'],
21+
['inspect','--probe','--help'],
22+
['inspect','--probe','-h'],
23+
];
24+
25+
for(constargsofhelpArgs){
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--probemustbefollowedimmediatelyby--expr/,
1818
trim: true,
1919
});

0 commit comments

Comments
 (0)