GeezerVison is showing github.com — debugger: add --help to `node inspect` and improve docs · nodejs/node@141de35
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file 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 Copy file name to clipboardExpand all lines: lib/internal/debugger/inspect.js +71-6Lines changed: 71 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -27,10 +27,11 @@ const { | 27 | 27 | const InspectClient = require('internal/debugger/inspect_client'); | 28 | 28 | const { | 29 | 29 | launchChildProcess, | 30 | | - writeUsageAndExit, | | 30 | + writeInspectUsageAndExit, | 31 | 31 | } = require('internal/debugger/inspect_helpers'); | 32 | 32 | const { | 33 | | - startProbeMode, | | 33 | + parseProbeTokens, | | 34 | + runProbeMode, | 34 | 35 | } = require('internal/debugger/inspect_probe'); | 35 | 36 | const createRepl = require('internal/debugger/inspect_repl'); | 36 | 37 |
| @@ -39,6 +40,7 @@ const debuglog = util.debuglog('inspect'); | 39 | 40 | const { | 40 | 41 | exitCodes: { | 41 | 42 | kGenericUserError, | | 43 | + kInvalidCommandLineArgument, | 42 | 44 | kNoFailure, | 43 | 45 | }, | 44 | 46 | } = internalBinding('errors'); | @@ -220,7 +222,7 @@ class NodeInspector { | 220 | 222 | } | 221 | 223 | } | 222 | 224 |
| 223 | | -function parseArgv(args) { | | 225 | +function parseInteractiveArgs(args) { | 224 | 226 | const target = ArrayPrototypeShift(args); | 225 | 227 | let host = '127.0.0.1'; | 226 | 228 | let port = 9229; | @@ -264,20 +266,83 @@ function parseArgv(args) { | 264 | 266 | }; | 265 | 267 | } | 266 | 268 |
| | 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 | + | 267 | 319 | function startInspect(argv = ArrayPrototypeSlice(process.argv, 2), | 268 | 320 | stdin = process.stdin, | 269 | 321 | stdout = process.stdout) { | 270 | 322 | const invokedAs = `${process.argv0} ${process.argv[1]}`; | 271 | 323 |
| 272 | 324 | if (argv.length < 1) { | 273 | | - writeUsageAndExit(invokedAs); | | 325 | + writeInspectUsageAndExit(invokedAs, undefined, kInvalidCommandLineArgument); | 274 | 326 | } | 275 | 327 |
| 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); | 277 | 342 | return; | 278 | 343 | } | 279 | 344 |
| 280 | | - const options = parseArgv(argv); | | 345 | + const options = parseInteractiveArgs(argv); | 281 | 346 | const inspector = new NodeInspector(options, stdin, stdout); | 282 | 347 |
| 283 | 348 | stdin.resume(); | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/debugger/inspect_helpers.js +60-14Lines changed: 60 additions & 14 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -61,21 +61,67 @@ function ensureTrailingNewline(text) { | 61 | 61 | return StringPrototypeEndsWith(text, '\n') ? text : `${text}\n`; | 62 | 62 | } | 63 | 63 |
| 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; | 65 | 67 | if (message) { | 66 | | - process.stderr.write(`${message}\n`); | | 68 | + out.write(`${message}\n`); | 67 | 69 | } | 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); | 79 | 125 | } | 80 | 126 |
| 81 | 127 | async function launchChildProcess(childArgs, inspectHost, inspectPort, | @@ -128,5 +174,5 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort, | 128 | 174 | module.exports = { | 129 | 175 | ensureTrailingNewline, | 130 | 176 | launchChildProcess, | 131 | | - writeUsageAndExit, | | 177 | + writeInspectUsageAndExit, | 132 | 178 | }; |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/debugger/inspect_probe.js +3-61Lines changed: 3 additions & 61 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -23,14 +23,12 @@ const { | 23 | 23 | } = primordials; | 24 | 24 |
| 25 | 25 | const { clearTimeout, setTimeout } = require('timers'); | 26 | | -const util = require('util'); | 27 | 26 | const { SideEffectFreeRegExpPrototypeSymbolReplace } = require('internal/util'); | 28 | 27 |
| 29 | 28 | const InspectClient = require('internal/debugger/inspect_client'); | 30 | 29 | const { | 31 | 30 | ensureTrailingNewline, | 32 | 31 | launchChildProcess, | 33 | | - writeUsageAndExit, | 34 | 32 | } = require('internal/debugger/inspect_helpers'); | 35 | 33 |
| 36 | 34 | const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes; | @@ -46,17 +44,6 @@ const kProbeVersion = 1; | 46 | 44 | const kProbeDisconnectSentinel = 'Waiting for the debugger to disconnect...'; | 47 | 45 | const kDigitsRegex = /^\d+$/; | 48 | 46 | 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 | | -}; | 60 | 47 |
| 61 | 48 | function parseUnsignedInteger(value, name, allowZero = false) { | 62 | 49 | if (typeof value !== 'string' || RegExpPrototypeExec(kDigitsRegex, value) === null) { | @@ -274,29 +261,7 @@ function buildProbeTextReport(report) { | 274 | 261 | return ensureTrailingNewline(ArrayPrototypeJoin(lines, '\n')); | 275 | 262 | } | 276 | 263 |
| 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) { | 300 | 265 | let port = 0; | 301 | 266 | let preview = false; | 302 | 267 | let timeout = kProbeDefaultTimeout; | @@ -307,14 +272,6 @@ function parseProbeArgv(args) { | 307 | 272 | let expectedExprIndex = -1; | 308 | 273 | const probes = []; | 309 | 274 |
| 310 | | - const { tokens } = util.parseArgs({ | 311 | | - args, | 312 | | - allowPositionals: true, | 313 | | - options: kProbeArgOptions, | 314 | | - strict: false, | 315 | | - tokens: true, | 316 | | - }); | 317 | | - | 318 | 275 | for (const token of tokens) { | 319 | 276 | if (token.kind === 'option-terminator') { | 320 | 277 | sawSeparator = true; | @@ -764,22 +721,7 @@ async function runProbeMode(stdout, probeOptions) { | 764 | 721 | } | 765 | 722 | } | 766 | 723 |
| 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 | | - | 783 | 724 | module.exports = { | 784 | | - startProbeMode, | | 725 | + parseProbeTokens, | | 726 | + runProbeMode, | 785 | 727 | }; |
Collapse file Copy file name to clipboard +34Lines changed: 34 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 Copy file name to clipboard +37Lines changed: 37 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 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 number | Diff line number | Diff line change |
|---|
@@ -13,7 +13,7 @@ spawnSyncAndExit(process.execPath, [ | 13 | 13 | probeScript, | 14 | 14 | ], { | 15 | 15 | signal: null, | 16 | | - status: 1, | | 16 | + status: 9, | 17 | 17 | stderr: /Each --probe must be followed immediately by --expr/, | 18 | 18 | trim: true, | 19 | 19 | }); |
|