Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
387 changes: 253 additions & 134 deletions doc/api/debugger.md

Large diffs are not rendered by default.

77 changes: 71 additions & 6 deletions lib/internal/debugger/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ const {
const InspectClient = require('internal/debugger/inspect_client');
const {
launchChildProcess,
writeUsageAndExit,
writeInspectUsageAndExit,
} = require('internal/debugger/inspect_helpers');
const {
startProbeMode,
parseProbeTokens,
runProbeMode,
} = require('internal/debugger/inspect_probe');
const createRepl = require('internal/debugger/inspect_repl');

Expand All @@ -39,6 +40,7 @@ const debuglog = util.debuglog('inspect');
const {
exitCodes: {
kGenericUserError,
kInvalidCommandLineArgument,
kNoFailure,
},
} = internalBinding('errors');
Expand Down Expand Up @@ -220,7 +222,7 @@ class NodeInspector {
}
}

function parseArgv(args) {
function parseInteractiveArgs(args) {
const target = ArrayPrototypeShift(args);
let host = '127.0.0.1';
let port = 9229;
Expand Down Expand Up @@ -264,20 +266,83 @@ function parseArgv(args) {
};
}

const kInspectArgOptions = {
__proto__: null,
expr: { type: 'string' },
help: { type: 'boolean', short: 'h' },
json: { type: 'boolean' },
// Port and timeout use type 'string' because parseArgs has no
// numeric type; the values are parsed to integers by parseProbeTokens().
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) {
writeUsageAndExit(invokedAs);
writeInspectUsageAndExit(invokedAs, undefined, kInvalidCommandLineArgument);
}

if (startProbeMode(invokedAs, argv, stdout)) {
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 = parseArgv(argv);
const options = parseInteractiveArgs(argv);
const inspector = new NodeInspector(options, stdin, stdout);

stdin.resume();
Expand Down
74 changes: 60 additions & 14 deletions lib/internal/debugger/inspect_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,67 @@ function ensureTrailingNewline(text) {
return StringPrototypeEndsWith(text, '\n') ? text : `${text}\n`;
}

function writeUsageAndExit(invokedAs, message, exitCode = kInvalidCommandLineArgument) {
function writeInspectUsageAndExit(invokedAs, message, exitCode) {
const code = exitCode ?? (message ? kInvalidCommandLineArgument : 0);
const out = code === 0 ? process.stdout : process.stderr;
if (message) {
process.stderr.write(`${message}\n`);
out.write(`${message}\n`);
}
const text =
`Usage: ${invokedAs} script.js\n` +
` ${invokedAs} <host>:<port>\n` +
` ${invokedAs} --port=<port> Use 0 for random port assignment\n` +
` ${invokedAs} -p <pid>\n` +
` ${invokedAs} [--json] [--timeout=<ms>] [--port=<port>] ` +
`--probe <file>:<line>[:<col>] --expr <expr> ` +
`[--probe <file>:<line>[:<col>] --expr <expr> ...] ` +
`[--] [<node-option> ...] <script.js> [args...]\n`;
process.stderr.write(text);
process.exit(exitCode);
out.write(`Usage: ${invokedAs} [--port=<port>] [<node-option> ...]
[<script> [<script-args>] | <host>:<port> | -p <pid>]
${invokedAs} --probe <file>:<line>[:<col>] --expr <expr>
[--probe <file>:<line>[:<col>] --expr <expr> ...]
[--json] [--preview] [--timeout=<ms>] [--port=<port>]
[--] [<node-option> ...] <script> [<script-args> ...]

Interactive mode: Starts a live debugging session.

Example:
$ node inspect script.js

Options:
--port=<port> Inspector port for the debuggee (default: 9229)
<script> The script to launch and debug.
<host>:<port> Remote debugger to connect to.
-p <pid> Attach to a running Node.js process by PID

Semantics:
* If neither a script nor a host:port nor -p is provided, node inspect starts
the REPL.

Non-interactive probe mode: Evaluates expressions whenever execution reaches
specified source locations and prints all the evaluation results to stdout.

Example:
$ node inspect --probe app.js:10 --expr "user"
--probe src/utils.js:5:15 --expr "config.options"
--json --preview -- --no-warnings app.js --arg-for-app=foo

Options:
--probe <file>:<line>[:<col>]
Source location of the probe (1-based, col defaults
to 1). Matches by file basename, use a fuller path to
disambiguate. Must be immediately followed by --expr.
--expr <expr> Expression to evaluate in the lexical scope of the
preceding --probe each time execution reaches it.
Avoid probing let/const-bound variables at their
declaration site or a ReferenceError may be thrown.
--json Output JSON if specified, otherwise human-readable text.
--preview Include V8 object previews in JSON output.
--timeout <ms> Global session timeout (default: 30000).
--port <port> Inspector port for the debuggee (default: 0 = random).

Semantics:
* Multiple --probe/--expr pairs are allowed. Same-location --probes share
a pause and scope, their --exprs are evaluated in command-line order.
* Use -- before any Node.js flags intended for the child process.
* Target errors are surfaced in the report as a terminal 'error' event.
The probing process exits 0 unless it encounters an error itself.

See https://nodejs.org/api/debugger.html for details, including the
probe output schema.
`);
process.exit(code);
}

async function launchChildProcess(childArgs, inspectHost, inspectPort,
Expand Down Expand Up @@ -128,5 +174,5 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
module.exports = {
ensureTrailingNewline,
launchChildProcess,
writeUsageAndExit,
writeInspectUsageAndExit,
};
64 changes: 3 additions & 61 deletions lib/internal/debugger/inspect_probe.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ const {
} = primordials;

const { clearTimeout, setTimeout } = require('timers');
const util = require('util');
const { SideEffectFreeRegExpPrototypeSymbolReplace } = require('internal/util');

const InspectClient = require('internal/debugger/inspect_client');
const {
ensureTrailingNewline,
launchChildProcess,
writeUsageAndExit,
} = require('internal/debugger/inspect_helpers');

const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes;
Expand All @@ -46,17 +44,6 @@ const kProbeVersion = 1;
const kProbeDisconnectSentinel = 'Waiting for the debugger to disconnect...';
const kDigitsRegex = /^\d+$/;
const kInspectPortRegex = /^--inspect-port=(\d+)$/;
const kProbeArgOptions = {
__proto__: null,
expr: { type: 'string' },
json: { type: 'boolean' },
// Port and timeout use type 'string' because parseArgs has no
// numeric type; the values are parsed to integers in parseProbeArgv().
port: { type: 'string' },
preview: { type: 'boolean' },
probe: { type: 'string' },
timeout: { type: 'string' },
};

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

function hasTopLevelProbeOption(args) {
const { tokens } = util.parseArgs({
args,
allowPositionals: true,
options: kProbeArgOptions,
strict: false,
tokens: true,
});

for (const token of tokens) {
if (token.kind === 'option' && token.name === 'probe') {
return true;
}

if (token.kind === 'option-terminator' || token.kind === 'positional') {
return false;
}
}

return false;
}

function parseProbeArgv(args) {
function parseProbeTokens(tokens, args) {
let port = 0;
let preview = false;
let timeout = kProbeDefaultTimeout;
Expand All @@ -307,14 +272,6 @@ function parseProbeArgv(args) {
let expectedExprIndex = -1;
const probes = [];

const { tokens } = util.parseArgs({
args,
allowPositionals: true,
options: kProbeArgOptions,
strict: false,
tokens: true,
});

for (const token of tokens) {
if (token.kind === 'option-terminator') {
sawSeparator = true;
Expand Down Expand Up @@ -764,22 +721,7 @@ async function runProbeMode(stdout, probeOptions) {
}
}

function startProbeMode(invokedAs, args, stdout) {
if (!hasTopLevelProbeOption(args)) {
return false;
}

let probeOptions;
try {
probeOptions = parseProbeArgv(args);
} catch (error) {
writeUsageAndExit(invokedAs, error.message, kGenericUserError);
}

runProbeMode(stdout, probeOptions);
return true;
}

module.exports = {
startProbeMode,
parseProbeTokens,
runProbeMode,
};
34 changes: 34 additions & 0 deletions test/parallel/test-debugger-inspect-help-forwarding.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Tests that --help / -h are forwarded to the debuggee when a script is
// also passed, instead of being hijacked as the inspect help flag.

import { skipIfInspectorDisabled } from '../common/index.mjs';

skipIfInspectorDisabled();

import * as fixtures from '../common/fixtures.mjs';
import startCLI from '../common/debugger.js';

import assert from 'assert';

async function getEvaluatedArgv(flag) {
const script = fixtures.path('debugger', 'empty.js');
const cli = startCLI([script, flag]);
try {
await cli.waitForInitialBreak();
await cli.waitForPrompt();
await cli.command('exec process.argv');
return cli.output;
} finally {
await cli.quit();
}
}

async function checkForwardedHelp(flag) {
const output = await getEvaluatedArgv(flag);
assert(output.includes(`'${flag}'`),
`expected debuggee process.argv to include "${flag}", got:\n${output}`);
assert.doesNotMatch(output, /Usage: .+ inspect/);
}

await checkForwardedHelp('--help');
await checkForwardedHelp('-h');
37 changes: 37 additions & 0 deletions test/parallel/test-debugger-inspect-help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

// Tests that `node inspect` help text is printed when
// `--help` / `-h` is passed and there is no script,
// or when there are no additional arguments.
const common = require('../common');
common.skipIfInspectorDisabled();

const {
spawnSyncAndAssert,
spawnSyncAndExit,
} = require('../common/child_process');

const usageRegex = /^Usage: .+ inspect .*debugger\.html/ms;

// --help / -h prints the usage to stdout and exits with code 0,
// for both interactive and probe-mode invocations.
const helpArgs = [
['inspect', '--help'],
['inspect', '-h'],
['inspect', '--probe', '--help'],
['inspect', '--probe', '-h'],
];

for (const args of helpArgs) {
spawnSyncAndAssert(process.execPath, args, {
stdout: usageRegex,
});
}

// `node inspect` with no args prints the usage to stderr and exits
// with kInvalidCommandLineArgument (9).
spawnSyncAndExit(process.execPath, ['inspect'], {
status: 9,
signal: null,
stderr: usageRegex,
});
2 changes: 1 addition & 1 deletion test/parallel/test-debugger-probe-missing-expr.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ spawnSyncAndExit(process.execPath, [
probeScript,
], {
signal: null,
status: 1,
status: 9,
stderr: /Each --probe must be followed immediately by --expr/,
trim: true,
});
Loading
Loading