mirror of
https://github.com/nodejs/node.git
synced 2025-05-04 20:09:32 +00:00

The term "native module" dates back to some of the oldest code in the code base. Within the context of Node.js core it usually refers to modules that are native to Node.js (e.g. fs, http), but it can cause confusion for people who don't work on this part of the code base, as "native module" can also refer to native addons - which is even the case in some of the API docs and error messages. This patch tries to make the usage of these terms more consistent. Now within the context of Node.js core: - JavaScript scripts that are built-in to Node.js are now referred to as "built-in(s)". If they are available as modules, they can also be referred to as "built-in module(s)". - Dynamically-linked shared objects that are loaded into the Node.js processes are referred to as "addons". We will try to avoid using the term "native modules" because it could be ambiguous. Changes in this patch: File names: - node_native_module.h -> node_builtins.h, - node_native_module.cc -> node_builtins.cc C++ binding names: - `native_module` -> `builtins` `node::Environment`: - `native_modules_without_cache` -> `builtins_without_cache` - `native_modules_with_cache` -> `builtins_with_cache` - `native_modules_in_snapshot` -> `builtins_in_cache` - `native_module_require` -> `builtin_module_require` `node::EnvSerializeInfo`: - `native_modules` -> `builtins `node::native_module::NativeModuleLoader`: - `native_module` namespace -> `builtins` namespace - `NativeModuleLoader` -> `BuiltinLoader` - `NativeModuleRecordMap` -> `BuiltinSourceMap` - `NativeModuleCacheMap` -> `BuiltinCodeCacheMap` - `ModuleIds` -> `BuiltinIds` - `ModuleCategories` -> `BuiltinCategories` - `LoadBuiltinModuleSource` -> `LoadBuiltinSource` `loader.js`: - `NativeModule` -> `BuiltinModule` (the `NativeModule` name used in `process.moduleLoadList` is kept for compatibility) And other clarifications in the documentation and comments. PR-URL: https://github.com/nodejs/node/pull/44135 Fixes: https://github.com/nodejs/node/issues/44036 Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Richard Lau <rlau@redhat.com> Reviewed-By: Jiawen Geng <technicalcute@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Mohammed Keyvanzadeh <mohammadkeyvanzade94@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Jan Krems <jan.krems@gmail.com>
1211 lines
35 KiB
JavaScript
1211 lines
35 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
Array,
|
|
ArrayFrom,
|
|
ArrayPrototypeFilter,
|
|
ArrayPrototypeFind,
|
|
ArrayPrototypeForEach,
|
|
ArrayPrototypeIncludes,
|
|
ArrayPrototypeIndexOf,
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypeMap,
|
|
ArrayPrototypePush,
|
|
ArrayPrototypeSlice,
|
|
ArrayPrototypeSome,
|
|
ArrayPrototypeSplice,
|
|
Date,
|
|
FunctionPrototypeCall,
|
|
JSONStringify,
|
|
MathMax,
|
|
ObjectAssign,
|
|
ObjectDefineProperty,
|
|
ObjectKeys,
|
|
ObjectValues,
|
|
Promise,
|
|
PromisePrototypeCatch,
|
|
PromisePrototypeThen,
|
|
PromiseResolve,
|
|
ReflectGetOwnPropertyDescriptor,
|
|
ReflectOwnKeys,
|
|
RegExpPrototypeExec,
|
|
RegExpPrototypeSymbolReplace,
|
|
SafeMap,
|
|
SafePromiseAll,
|
|
String,
|
|
StringFromCharCode,
|
|
StringPrototypeEndsWith,
|
|
StringPrototypeIncludes,
|
|
StringPrototypeRepeat,
|
|
StringPrototypeSlice,
|
|
StringPrototypeSplit,
|
|
StringPrototypeStartsWith,
|
|
StringPrototypeToUpperCase,
|
|
StringPrototypeTrim,
|
|
} = primordials;
|
|
|
|
const { ERR_DEBUGGER_ERROR } = require('internal/errors').codes;
|
|
|
|
const { validateString } = require('internal/validators');
|
|
|
|
const FS = require('fs');
|
|
const Path = require('path');
|
|
const Repl = require('repl');
|
|
const vm = require('vm');
|
|
const { fileURLToPath } = require('internal/url');
|
|
|
|
const { customInspectSymbol } = require('internal/util');
|
|
const { inspect: utilInspect } = require('internal/util/inspect');
|
|
const debuglog = require('internal/util/debuglog').debuglog('inspect');
|
|
|
|
const SHORTCUTS = {
|
|
cont: 'c',
|
|
next: 'n',
|
|
step: 's',
|
|
out: 'o',
|
|
backtrace: 'bt',
|
|
setBreakpoint: 'sb',
|
|
clearBreakpoint: 'cb',
|
|
run: 'r',
|
|
exec: 'p'
|
|
};
|
|
|
|
const HELP = StringPrototypeTrim(`
|
|
run, restart, r Run the application or reconnect
|
|
kill Kill a running application or disconnect
|
|
|
|
cont, c Resume execution
|
|
next, n Continue to next line in current file
|
|
step, s Step into, potentially entering a function
|
|
out, o Step out, leaving the current function
|
|
backtrace, bt Print the current backtrace
|
|
list Print the source around the current line where execution
|
|
is currently paused
|
|
|
|
setBreakpoint, sb Set a breakpoint
|
|
clearBreakpoint, cb Clear a breakpoint
|
|
breakpoints List all known breakpoints
|
|
breakOnException Pause execution whenever an exception is thrown
|
|
breakOnUncaught Pause execution whenever an exception isn't caught
|
|
breakOnNone Don't pause on exceptions (this is the default)
|
|
|
|
watch(expr) Start watching the given expression
|
|
unwatch(expr) Stop watching an expression
|
|
watchers Print all watched expressions and their current values
|
|
|
|
exec(expr), p(expr), exec expr, p expr
|
|
Evaluate the expression and print the value
|
|
repl Enter a debug repl that works like exec
|
|
|
|
scripts List application scripts that are currently loaded
|
|
scripts(true) List all scripts (including node-internals)
|
|
|
|
profile Start CPU profiling session.
|
|
profileEnd Stop current CPU profiling session.
|
|
profiles Array of completed CPU profiling sessions.
|
|
profiles[n].save(filepath = 'node.cpuprofile')
|
|
Save CPU profiling session to disk as JSON.
|
|
|
|
takeHeapSnapshot(filepath = 'node.heapsnapshot')
|
|
Take a heap snapshot and save to disk as JSON.
|
|
`);
|
|
|
|
const FUNCTION_NAME_PATTERN = /^(?:function\*? )?([^(\s]+)\(/;
|
|
function extractFunctionName(description) {
|
|
const fnNameMatch =
|
|
RegExpPrototypeExec(FUNCTION_NAME_PATTERN, description);
|
|
return fnNameMatch ? `: ${fnNameMatch[1]}` : '';
|
|
}
|
|
|
|
const {
|
|
builtinIds: PUBLIC_BUILTINS,
|
|
} = internalBinding('builtins');
|
|
const NATIVES = internalBinding('natives');
|
|
function isNativeUrl(url) {
|
|
url = RegExpPrototypeSymbolReplace(/\.js$/, url, '');
|
|
|
|
return StringPrototypeStartsWith(url, 'node:internal/') ||
|
|
ArrayPrototypeIncludes(PUBLIC_BUILTINS, url) ||
|
|
url in NATIVES || url === 'bootstrap_node';
|
|
}
|
|
|
|
function getRelativePath(filenameOrURL) {
|
|
const dir = StringPrototypeSlice(Path.join(Path.resolve(), 'x'), 0, -1);
|
|
|
|
const filename = StringPrototypeStartsWith(filenameOrURL, 'file://') ?
|
|
fileURLToPath(filenameOrURL) : filenameOrURL;
|
|
|
|
// Change path to relative, if possible
|
|
if (StringPrototypeStartsWith(filename, dir)) {
|
|
return StringPrototypeSlice(filename, dir.length);
|
|
}
|
|
return filename;
|
|
}
|
|
|
|
// Adds spaces and prefix to number
|
|
// maxN is a maximum number we should have space for
|
|
function leftPad(n, prefix, maxN) {
|
|
const s = n.toString();
|
|
const nchars = MathMax(2, String(maxN).length);
|
|
const nspaces = nchars - s.length;
|
|
|
|
return prefix + StringPrototypeRepeat(' ', nspaces) + s;
|
|
}
|
|
|
|
function markSourceColumn(sourceText, position, useColors) {
|
|
if (!sourceText) return '';
|
|
|
|
const head = StringPrototypeSlice(sourceText, 0, position);
|
|
let tail = StringPrototypeSlice(sourceText, position);
|
|
|
|
// Colourize char if stdout supports colours
|
|
if (useColors) {
|
|
tail = RegExpPrototypeSymbolReplace(/(.+?)([^\w]|$)/, tail,
|
|
'\u001b[32m$1\u001b[39m$2');
|
|
}
|
|
|
|
// Return source line with coloured char at `position`
|
|
return head + tail;
|
|
}
|
|
|
|
function extractErrorMessage(stack) {
|
|
if (!stack) return '<unknown>';
|
|
const m = RegExpPrototypeExec(/^\w+: ([^\n]+)/, stack);
|
|
return m?.[1] ?? stack;
|
|
}
|
|
|
|
function convertResultToError(result) {
|
|
const { className, description } = result;
|
|
const err = new ERR_DEBUGGER_ERROR(extractErrorMessage(description));
|
|
err.stack = description;
|
|
ObjectDefineProperty(err, 'name', { __proto__: null, value: className });
|
|
return err;
|
|
}
|
|
|
|
class PropertyPreview {
|
|
constructor(attributes) {
|
|
ObjectAssign(this, attributes);
|
|
}
|
|
|
|
[customInspectSymbol](depth, opts) {
|
|
switch (this.type) {
|
|
case 'string':
|
|
case 'undefined':
|
|
return utilInspect(this.value, opts);
|
|
case 'number':
|
|
case 'boolean':
|
|
return opts.stylize(this.value, this.type);
|
|
case 'object':
|
|
case 'symbol':
|
|
if (this.subtype === 'date') {
|
|
return utilInspect(new Date(this.value), opts);
|
|
}
|
|
if (this.subtype === 'array') {
|
|
return opts.stylize(this.value, 'special');
|
|
}
|
|
return opts.stylize(this.value, this.subtype || 'special');
|
|
default:
|
|
return this.value;
|
|
}
|
|
}
|
|
}
|
|
|
|
class ObjectPreview {
|
|
constructor(attributes) {
|
|
ObjectAssign(this, attributes);
|
|
}
|
|
|
|
[customInspectSymbol](depth, opts) {
|
|
switch (this.type) {
|
|
case 'object': {
|
|
switch (this.subtype) {
|
|
case 'date':
|
|
return utilInspect(new Date(this.description), opts);
|
|
case 'null':
|
|
return utilInspect(null, opts);
|
|
case 'regexp':
|
|
return opts.stylize(this.description, 'regexp');
|
|
case 'set': {
|
|
if (!this.entries) {
|
|
return `${this.description} ${this.overflow ? '{ ... }' : '{}'}`;
|
|
}
|
|
const values = ArrayPrototypeMap(this.entries, (entry) =>
|
|
utilInspect(new ObjectPreview(entry.value), opts));
|
|
return `${this.description} { ${ArrayPrototypeJoin(values, ', ')} }`;
|
|
}
|
|
case 'map': {
|
|
if (!this.entries) {
|
|
return `${this.description} ${this.overflow ? '{ ... }' : '{}'}`;
|
|
}
|
|
const mappings = ArrayPrototypeMap(this.entries, (entry) => {
|
|
const key = utilInspect(new ObjectPreview(entry.key), opts);
|
|
const value = utilInspect(new ObjectPreview(entry.value), opts);
|
|
return `${key} => ${value}`;
|
|
});
|
|
return `${this.description} { ${ArrayPrototypeJoin(mappings, ', ')} }`;
|
|
}
|
|
case 'array':
|
|
case undefined: {
|
|
if (this.properties.length === 0) {
|
|
return this.subtype === 'array' ? '[]' : '{}';
|
|
}
|
|
const props = ArrayPrototypeMap(this.properties, (prop, idx) => {
|
|
const value = utilInspect(new PropertyPreview(prop));
|
|
if (prop.name === `${idx}`) return value;
|
|
return `${prop.name}: ${value}`;
|
|
});
|
|
if (this.overflow) {
|
|
ArrayPrototypePush(props, '...');
|
|
}
|
|
const singleLine = ArrayPrototypeJoin(props, ', ');
|
|
const propString = singleLine.length > 60 ? ArrayPrototypeJoin(props, ',\n ') : singleLine;
|
|
return this.subtype === 'array' ? `[ ${propString} ]` : `{ ${propString} }`;
|
|
}
|
|
default:
|
|
return this.description;
|
|
}
|
|
}
|
|
default:
|
|
return this.description;
|
|
}
|
|
}
|
|
}
|
|
|
|
class RemoteObject {
|
|
constructor(attributes) {
|
|
ObjectAssign(this, attributes);
|
|
if (this.type === 'number') {
|
|
this.value =
|
|
this.unserializableValue ? +this.unserializableValue : +this.value;
|
|
}
|
|
}
|
|
|
|
[customInspectSymbol](depth, opts) {
|
|
switch (this.type) {
|
|
case 'boolean':
|
|
case 'number':
|
|
case 'string':
|
|
case 'undefined':
|
|
return utilInspect(this.value, opts);
|
|
case 'symbol':
|
|
return opts.stylize(this.description, 'special');
|
|
case 'function': {
|
|
const fnName = extractFunctionName(this.description);
|
|
const formatted = `[${this.className}${fnName}]`;
|
|
return opts.stylize(formatted, 'special');
|
|
}
|
|
case 'object':
|
|
switch (this.subtype) {
|
|
case 'date':
|
|
return utilInspect(new Date(this.description), opts);
|
|
case 'null':
|
|
return utilInspect(null, opts);
|
|
case 'regexp':
|
|
return opts.stylize(this.description, 'regexp');
|
|
case 'map':
|
|
case 'set': {
|
|
const preview = utilInspect(new ObjectPreview(this.preview), opts);
|
|
return `${this.description} ${preview}`;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
if (this.preview) {
|
|
return utilInspect(new ObjectPreview(this.preview), opts);
|
|
}
|
|
return this.description;
|
|
default:
|
|
return this.description;
|
|
}
|
|
}
|
|
|
|
static fromEvalResult({ result, wasThrown }) {
|
|
if (wasThrown) return convertResultToError(result);
|
|
return new RemoteObject(result);
|
|
}
|
|
}
|
|
|
|
class ScopeSnapshot {
|
|
constructor(scope, properties) {
|
|
ObjectAssign(this, scope);
|
|
this.properties = new SafeMap();
|
|
this.completionGroup = ArrayPrototypeMap(properties, (prop) => {
|
|
const value = new RemoteObject(prop.value);
|
|
this.properties.set(prop.name, value);
|
|
return prop.name;
|
|
});
|
|
}
|
|
|
|
[customInspectSymbol](depth, opts) {
|
|
const type = StringPrototypeToUpperCase(this.type[0]) +
|
|
StringPrototypeSlice(this.type, 1);
|
|
const name = this.name ? `<${this.name}>` : '';
|
|
const prefix = `${type}${name} `;
|
|
return RegExpPrototypeSymbolReplace(/^Map /,
|
|
utilInspect(this.properties, opts),
|
|
prefix);
|
|
}
|
|
}
|
|
|
|
function copyOwnProperties(target, source) {
|
|
ArrayPrototypeForEach(
|
|
ReflectOwnKeys(source),
|
|
(prop) => {
|
|
const desc = ReflectGetOwnPropertyDescriptor(source, prop);
|
|
ObjectDefineProperty(target, prop, desc);
|
|
});
|
|
}
|
|
|
|
function aliasProperties(target, mapping) {
|
|
ArrayPrototypeForEach(ObjectKeys(mapping), (key) => {
|
|
const desc = ReflectGetOwnPropertyDescriptor(target, key);
|
|
ObjectDefineProperty(target, mapping[key], desc);
|
|
});
|
|
}
|
|
|
|
function createRepl(inspector) {
|
|
const { Debugger, HeapProfiler, Profiler, Runtime } = inspector;
|
|
|
|
let repl;
|
|
|
|
// Things we want to keep around
|
|
const history = { control: [], debug: [] };
|
|
const watchedExpressions = [];
|
|
const knownBreakpoints = [];
|
|
let heapSnapshotPromise = null;
|
|
let pauseOnExceptionState = 'none';
|
|
let lastCommand;
|
|
|
|
// Things we need to reset when the app restarts
|
|
let knownScripts;
|
|
let currentBacktrace;
|
|
let selectedFrame;
|
|
let exitDebugRepl;
|
|
|
|
function resetOnStart() {
|
|
knownScripts = {};
|
|
currentBacktrace = null;
|
|
selectedFrame = null;
|
|
|
|
if (exitDebugRepl) exitDebugRepl();
|
|
exitDebugRepl = null;
|
|
}
|
|
resetOnStart();
|
|
|
|
const INSPECT_OPTIONS = { colors: inspector.stdout.isTTY };
|
|
function inspect(value) {
|
|
return utilInspect(value, INSPECT_OPTIONS);
|
|
}
|
|
|
|
function print(value, addNewline = true) {
|
|
const text = typeof value === 'string' ? value : inspect(value);
|
|
return inspector.print(text, addNewline);
|
|
}
|
|
|
|
function getCurrentLocation() {
|
|
if (!selectedFrame) {
|
|
throw new ERR_DEBUGGER_ERROR('Requires execution to be paused');
|
|
}
|
|
return selectedFrame.location;
|
|
}
|
|
|
|
function isCurrentScript(script) {
|
|
return selectedFrame && getCurrentLocation().scriptId === script.scriptId;
|
|
}
|
|
|
|
function formatScripts(displayNatives = false) {
|
|
function isVisible(script) {
|
|
if (displayNatives) return true;
|
|
return !script.isNative || isCurrentScript(script);
|
|
}
|
|
|
|
return ArrayPrototypeJoin(ArrayPrototypeMap(
|
|
ArrayPrototypeFilter(ObjectValues(knownScripts), isVisible),
|
|
(script) => {
|
|
const isCurrent = isCurrentScript(script);
|
|
const { isNative, url } = script;
|
|
const name = `${getRelativePath(url)}${isNative ? ' <native>' : ''}`;
|
|
return `${isCurrent ? '*' : ' '} ${script.scriptId}: ${name}`;
|
|
}), '\n');
|
|
}
|
|
|
|
function listScripts(displayNatives = false) {
|
|
print(formatScripts(displayNatives));
|
|
}
|
|
listScripts[customInspectSymbol] = function listWithoutInternal() {
|
|
return formatScripts();
|
|
};
|
|
|
|
const profiles = [];
|
|
class Profile {
|
|
constructor(data) {
|
|
this.data = data;
|
|
}
|
|
|
|
static createAndRegister({ profile }) {
|
|
const p = new Profile(profile);
|
|
ArrayPrototypePush(profiles, p);
|
|
return p;
|
|
}
|
|
|
|
[customInspectSymbol](depth, { stylize }) {
|
|
const { startTime, endTime } = this.data;
|
|
const MU = StringFromCharCode(956);
|
|
return stylize(`[Profile ${endTime - startTime}${MU}s]`, 'special');
|
|
}
|
|
|
|
save(filename = 'node.cpuprofile') {
|
|
const absoluteFile = Path.resolve(filename);
|
|
const json = JSONStringify(this.data);
|
|
FS.writeFileSync(absoluteFile, json);
|
|
print('Saved profile to ' + absoluteFile);
|
|
}
|
|
}
|
|
|
|
class SourceSnippet {
|
|
constructor(location, delta, scriptSource) {
|
|
ObjectAssign(this, location);
|
|
this.scriptSource = scriptSource;
|
|
this.delta = delta;
|
|
}
|
|
|
|
[customInspectSymbol](depth, options) {
|
|
const { scriptId, lineNumber, columnNumber, delta, scriptSource } = this;
|
|
const start = MathMax(1, lineNumber - delta + 1);
|
|
const end = lineNumber + delta + 1;
|
|
|
|
const lines = StringPrototypeSplit(scriptSource, '\n');
|
|
return ArrayPrototypeJoin(
|
|
ArrayPrototypeMap(
|
|
ArrayPrototypeSlice(lines, start - 1, end),
|
|
(lineText, offset) => {
|
|
const i = start + offset;
|
|
const isCurrent = i === (lineNumber + 1);
|
|
|
|
const markedLine = isCurrent ?
|
|
markSourceColumn(lineText, columnNumber, options.colors) :
|
|
lineText;
|
|
|
|
let isBreakpoint = false;
|
|
ArrayPrototypeForEach(knownBreakpoints, ({ location }) => {
|
|
if (!location) return;
|
|
if (scriptId === location.scriptId &&
|
|
i === (location.lineNumber + 1)) {
|
|
isBreakpoint = true;
|
|
}
|
|
});
|
|
|
|
let prefixChar = ' ';
|
|
if (isCurrent) {
|
|
prefixChar = '>';
|
|
} else if (isBreakpoint) {
|
|
prefixChar = '*';
|
|
}
|
|
return `${leftPad(i, prefixChar, end)} ${markedLine}`;
|
|
}), '\n');
|
|
}
|
|
}
|
|
|
|
async function getSourceSnippet(location, delta = 5) {
|
|
const { scriptId } = location;
|
|
const { scriptSource } = await Debugger.getScriptSource({ scriptId });
|
|
return new SourceSnippet(location, delta, scriptSource);
|
|
}
|
|
|
|
class CallFrame {
|
|
constructor(callFrame) {
|
|
ObjectAssign(this, callFrame);
|
|
}
|
|
|
|
loadScopes() {
|
|
return SafePromiseAll(
|
|
ArrayPrototypeFilter(
|
|
this.scopeChain,
|
|
(scope) => scope.type !== 'global'
|
|
),
|
|
async (scope) => {
|
|
const { objectId } = scope.object;
|
|
const { result } = await Runtime.getProperties({
|
|
objectId,
|
|
generatePreview: true,
|
|
});
|
|
return new ScopeSnapshot(scope, result);
|
|
});
|
|
}
|
|
|
|
list(delta = 5) {
|
|
return getSourceSnippet(this.location, delta);
|
|
}
|
|
}
|
|
|
|
class Backtrace extends Array {
|
|
[customInspectSymbol]() {
|
|
return ArrayPrototypeJoin(
|
|
ArrayPrototypeMap(this, (callFrame, idx) => {
|
|
const {
|
|
location: { scriptId, lineNumber, columnNumber },
|
|
functionName
|
|
} = callFrame;
|
|
const name = functionName || '(anonymous)';
|
|
|
|
const script = knownScripts[scriptId];
|
|
const relativeUrl =
|
|
(script && getRelativePath(script.url)) || '<unknown>';
|
|
const frameLocation =
|
|
`${relativeUrl}:${lineNumber + 1}:${columnNumber}`;
|
|
|
|
return `#${idx} ${name} ${frameLocation}`;
|
|
}), '\n');
|
|
}
|
|
|
|
static from(callFrames) {
|
|
return FunctionPrototypeCall(
|
|
ArrayFrom,
|
|
this,
|
|
callFrames,
|
|
(callFrame) =>
|
|
(callFrame instanceof CallFrame ?
|
|
callFrame :
|
|
new CallFrame(callFrame))
|
|
);
|
|
}
|
|
}
|
|
|
|
function prepareControlCode(input) {
|
|
if (input === '\n') return lastCommand;
|
|
// Add parentheses: exec process.title => exec("process.title");
|
|
const match = RegExpPrototypeExec(/^\s*(?:exec|p)\s+([^\n]*)/, input);
|
|
if (match) {
|
|
lastCommand = `exec(${JSONStringify(match[1])})`;
|
|
} else {
|
|
lastCommand = input;
|
|
}
|
|
return lastCommand;
|
|
}
|
|
|
|
async function evalInCurrentContext(code) {
|
|
// Repl asked for scope variables
|
|
if (code === '.scope') {
|
|
if (!selectedFrame) {
|
|
throw new ERR_DEBUGGER_ERROR('Requires execution to be paused');
|
|
}
|
|
const scopes = await selectedFrame.loadScopes();
|
|
return ArrayPrototypeMap(scopes, (scope) => scope.completionGroup);
|
|
}
|
|
|
|
if (selectedFrame) {
|
|
return PromisePrototypeThen(Debugger.evaluateOnCallFrame({
|
|
callFrameId: selectedFrame.callFrameId,
|
|
expression: code,
|
|
objectGroup: 'node-inspect',
|
|
generatePreview: true,
|
|
}), RemoteObject.fromEvalResult);
|
|
}
|
|
return PromisePrototypeThen(Runtime.evaluate({
|
|
expression: code,
|
|
objectGroup: 'node-inspect',
|
|
generatePreview: true,
|
|
}), RemoteObject.fromEvalResult);
|
|
}
|
|
|
|
function controlEval(input, context, filename, callback) {
|
|
debuglog('eval:', input);
|
|
function returnToCallback(error, result) {
|
|
debuglog('end-eval:', input, error);
|
|
callback(error, result);
|
|
}
|
|
|
|
try {
|
|
const code = prepareControlCode(input);
|
|
const result = vm.runInContext(code, context, filename);
|
|
|
|
const then = result?.then;
|
|
if (typeof then === 'function') {
|
|
FunctionPrototypeCall(
|
|
then, result,
|
|
(result) => returnToCallback(null, result),
|
|
returnToCallback
|
|
);
|
|
} else {
|
|
returnToCallback(null, result);
|
|
}
|
|
} catch (e) {
|
|
returnToCallback(e);
|
|
}
|
|
}
|
|
|
|
function debugEval(input, context, filename, callback) {
|
|
debuglog('eval:', input);
|
|
function returnToCallback(error, result) {
|
|
debuglog('end-eval:', input, error);
|
|
callback(error, result);
|
|
}
|
|
|
|
PromisePrototypeThen(evalInCurrentContext(input),
|
|
(result) => returnToCallback(null, result),
|
|
returnToCallback
|
|
);
|
|
}
|
|
|
|
async function formatWatchers(verbose = false) {
|
|
if (!watchedExpressions.length) {
|
|
return '';
|
|
}
|
|
|
|
const inspectValue = (expr) =>
|
|
PromisePrototypeCatch(evalInCurrentContext(expr),
|
|
(error) => `<${error.message}>`);
|
|
const lastIndex = watchedExpressions.length - 1;
|
|
|
|
const values = await SafePromiseAll(watchedExpressions, inspectValue);
|
|
const lines = ArrayPrototypeMap(watchedExpressions, (expr, idx) => {
|
|
const prefix = `${leftPad(idx, ' ', lastIndex)}: ${expr} =`;
|
|
const value = inspect(values[idx]);
|
|
if (!StringPrototypeIncludes(value, '\n')) {
|
|
return `${prefix} ${value}`;
|
|
}
|
|
return `${prefix}\n ${RegExpPrototypeSymbolReplace(/\n/g, value, '\n ')}`;
|
|
});
|
|
const valueList = ArrayPrototypeJoin(lines, '\n');
|
|
return verbose ? `Watchers:\n${valueList}\n` : valueList;
|
|
}
|
|
|
|
function watchers(verbose = false) {
|
|
return PromisePrototypeThen(formatWatchers(verbose), print);
|
|
}
|
|
|
|
// List source code
|
|
function list(delta = 5) {
|
|
if (!selectedFrame) {
|
|
throw new ERR_DEBUGGER_ERROR('Requires execution to be paused');
|
|
}
|
|
return selectedFrame.list(delta).then(null, (error) => {
|
|
print("You can't list source code right now");
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
function handleBreakpointResolved({ breakpointId, location }) {
|
|
const script = knownScripts[location.scriptId];
|
|
const scriptUrl = script && script.url;
|
|
if (scriptUrl) {
|
|
ObjectAssign(location, { scriptUrl });
|
|
}
|
|
const isExisting = ArrayPrototypeSome(knownBreakpoints, (bp) => {
|
|
if (bp.breakpointId === breakpointId) {
|
|
ObjectAssign(bp, { location });
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
if (!isExisting) {
|
|
ArrayPrototypePush(knownBreakpoints, { breakpointId, location });
|
|
}
|
|
}
|
|
|
|
function listBreakpoints() {
|
|
if (!knownBreakpoints.length) {
|
|
print('No breakpoints yet');
|
|
return;
|
|
}
|
|
|
|
function formatLocation(location) {
|
|
if (!location) return '<unknown location>';
|
|
const script = knownScripts[location.scriptId];
|
|
const scriptUrl = script ? script.url : location.scriptUrl;
|
|
return `${getRelativePath(scriptUrl)}:${location.lineNumber + 1}`;
|
|
}
|
|
const breaklist = ArrayPrototypeJoin(
|
|
ArrayPrototypeMap(
|
|
knownBreakpoints,
|
|
(bp, idx) => `#${idx} ${formatLocation(bp.location)}`),
|
|
'\n');
|
|
print(breaklist);
|
|
}
|
|
|
|
function setBreakpoint(script, line, condition, silent) {
|
|
function registerBreakpoint({ breakpointId, actualLocation }) {
|
|
handleBreakpointResolved({ breakpointId, location: actualLocation });
|
|
if (actualLocation && actualLocation.scriptId) {
|
|
if (!silent) return getSourceSnippet(actualLocation, 5);
|
|
} else {
|
|
print(`Warning: script '${script}' was not loaded yet.`);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// setBreakpoint(): set breakpoint at current location
|
|
if (script === undefined) {
|
|
return PromisePrototypeThen(
|
|
Debugger.setBreakpoint({ location: getCurrentLocation(), condition }),
|
|
registerBreakpoint);
|
|
}
|
|
|
|
// setBreakpoint(line): set breakpoint in current script at specific line
|
|
if (line === undefined && typeof script === 'number') {
|
|
const location = {
|
|
scriptId: getCurrentLocation().scriptId,
|
|
lineNumber: script - 1,
|
|
};
|
|
return PromisePrototypeThen(
|
|
Debugger.setBreakpoint({ location, condition }),
|
|
registerBreakpoint);
|
|
}
|
|
|
|
validateString(script, 'script');
|
|
|
|
// setBreakpoint('fn()'): Break when a function is called
|
|
if (StringPrototypeEndsWith(script, '()')) {
|
|
const debugExpr = `debug(${script.slice(0, -2)})`;
|
|
const debugCall = selectedFrame ?
|
|
Debugger.evaluateOnCallFrame({
|
|
callFrameId: selectedFrame.callFrameId,
|
|
expression: debugExpr,
|
|
includeCommandLineAPI: true,
|
|
}) :
|
|
Runtime.evaluate({
|
|
expression: debugExpr,
|
|
includeCommandLineAPI: true,
|
|
});
|
|
return PromisePrototypeThen(debugCall, ({ result, wasThrown }) => {
|
|
if (wasThrown) return convertResultToError(result);
|
|
return undefined; // This breakpoint can't be removed the same way
|
|
});
|
|
}
|
|
|
|
// setBreakpoint('scriptname')
|
|
let scriptId = null;
|
|
let ambiguous = false;
|
|
if (knownScripts[script]) {
|
|
scriptId = script;
|
|
} else {
|
|
ArrayPrototypeForEach(ObjectKeys(knownScripts), (id) => {
|
|
const scriptUrl = knownScripts[id].url;
|
|
if (scriptUrl && StringPrototypeIncludes(scriptUrl, script)) {
|
|
if (scriptId !== null) {
|
|
ambiguous = true;
|
|
}
|
|
scriptId = id;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (ambiguous) {
|
|
print('Script name is ambiguous');
|
|
return undefined;
|
|
}
|
|
if (line <= 0) {
|
|
print('Line should be a positive value');
|
|
return undefined;
|
|
}
|
|
|
|
if (scriptId !== null) {
|
|
const location = { scriptId, lineNumber: line - 1 };
|
|
return PromisePrototypeThen(
|
|
Debugger.setBreakpoint({ location, condition }),
|
|
registerBreakpoint);
|
|
}
|
|
|
|
const escapedPath = RegExpPrototypeSymbolReplace(/([/\\.?*()^${}|[\]])/g,
|
|
script, '\\$1');
|
|
const urlRegex = `^(.*[\\/\\\\])?${escapedPath}$`;
|
|
|
|
return PromisePrototypeThen(
|
|
Debugger.setBreakpointByUrl({
|
|
urlRegex,
|
|
lineNumber: line - 1,
|
|
condition,
|
|
}),
|
|
(bp) => {
|
|
// TODO: handle bp.locations in case the regex matches existing files
|
|
if (!bp.location) { // Fake it for now.
|
|
ObjectAssign(bp, {
|
|
actualLocation: {
|
|
scriptUrl: `.*/${script}$`,
|
|
lineNumber: line - 1,
|
|
},
|
|
});
|
|
}
|
|
return registerBreakpoint(bp);
|
|
});
|
|
}
|
|
|
|
function clearBreakpoint(url, line) {
|
|
const breakpoint = ArrayPrototypeFind(knownBreakpoints, ({ location }) => {
|
|
if (!location) return false;
|
|
const script = knownScripts[location.scriptId];
|
|
if (!script) return false;
|
|
return (
|
|
StringPrototypeIncludes(script.url, url) &&
|
|
(location.lineNumber + 1) === line
|
|
);
|
|
});
|
|
if (!breakpoint) {
|
|
print(`Could not find breakpoint at ${url}:${line}`);
|
|
return PromiseResolve();
|
|
}
|
|
return PromisePrototypeThen(
|
|
Debugger.removeBreakpoint({ breakpointId: breakpoint.breakpointId }),
|
|
() => {
|
|
const idx = ArrayPrototypeIndexOf(knownBreakpoints, breakpoint);
|
|
ArrayPrototypeSplice(knownBreakpoints, idx, 1);
|
|
});
|
|
}
|
|
|
|
function restoreBreakpoints() {
|
|
const lastBreakpoints = ArrayPrototypeSplice(knownBreakpoints, 0);
|
|
const newBreakpoints = ArrayPrototypeMap(
|
|
ArrayPrototypeFilter(lastBreakpoints,
|
|
({ location }) => !!location.scriptUrl),
|
|
({ location }) => setBreakpoint(location.scriptUrl,
|
|
location.lineNumber + 1));
|
|
if (!newBreakpoints.length) return PromiseResolve();
|
|
return PromisePrototypeThen(
|
|
SafePromiseAll(newBreakpoints),
|
|
(results) => {
|
|
print(`${results.length} breakpoints restored.`);
|
|
});
|
|
}
|
|
|
|
function setPauseOnExceptions(state) {
|
|
return PromisePrototypeThen(
|
|
Debugger.setPauseOnExceptions({ state }),
|
|
() => {
|
|
pauseOnExceptionState = state;
|
|
});
|
|
}
|
|
|
|
Debugger.on('paused', ({ callFrames, reason /* , hitBreakpoints */ }) => {
|
|
if (process.env.NODE_INSPECT_RESUME_ON_START === '1' &&
|
|
reason === 'Break on start') {
|
|
debuglog('Paused on start, but NODE_INSPECT_RESUME_ON_START' +
|
|
' environment variable is set to 1, resuming');
|
|
inspector.client.callMethod('Debugger.resume');
|
|
return;
|
|
}
|
|
|
|
// Save execution context's data
|
|
currentBacktrace = Backtrace.from(callFrames);
|
|
selectedFrame = currentBacktrace[0];
|
|
const { scriptId, lineNumber } = selectedFrame.location;
|
|
|
|
const breakType = reason === 'other' ? 'break' : reason;
|
|
const script = knownScripts[scriptId];
|
|
const scriptUrl = script ? getRelativePath(script.url) : '[unknown]';
|
|
|
|
const header = `${breakType} in ${scriptUrl}:${lineNumber + 1}`;
|
|
|
|
inspector.suspendReplWhile(() =>
|
|
PromisePrototypeThen(
|
|
SafePromiseAll([formatWatchers(true), selectedFrame.list(2)]),
|
|
({ 0: watcherList, 1: context }) => {
|
|
const breakContext = watcherList ?
|
|
`${watcherList}\n${inspect(context)}` :
|
|
inspect(context);
|
|
print(`${header}\n${breakContext}`);
|
|
}));
|
|
});
|
|
|
|
function handleResumed() {
|
|
currentBacktrace = null;
|
|
selectedFrame = null;
|
|
}
|
|
|
|
Debugger.on('resumed', handleResumed);
|
|
|
|
Debugger.on('breakpointResolved', handleBreakpointResolved);
|
|
|
|
Debugger.on('scriptParsed', (script) => {
|
|
const { scriptId, url } = script;
|
|
if (url) {
|
|
knownScripts[scriptId] = { isNative: isNativeUrl(url), ...script };
|
|
}
|
|
});
|
|
|
|
Profiler.on('consoleProfileFinished', ({ profile }) => {
|
|
Profile.createAndRegister({ profile });
|
|
print(
|
|
'Captured new CPU profile.\n' +
|
|
`Access it with profiles[${profiles.length - 1}]`
|
|
);
|
|
});
|
|
|
|
function initializeContext(context) {
|
|
ArrayPrototypeForEach(inspector.domainNames, (domain) => {
|
|
ObjectDefineProperty(context, domain, {
|
|
__proto__: null,
|
|
value: inspector[domain],
|
|
enumerable: true,
|
|
configurable: true,
|
|
writeable: false,
|
|
});
|
|
});
|
|
|
|
copyOwnProperties(context, {
|
|
get help() {
|
|
return print(HELP);
|
|
},
|
|
|
|
get run() {
|
|
return inspector.run();
|
|
},
|
|
|
|
get kill() {
|
|
return inspector.killChild();
|
|
},
|
|
|
|
get restart() {
|
|
return inspector.run();
|
|
},
|
|
|
|
get cont() {
|
|
handleResumed();
|
|
return Debugger.resume();
|
|
},
|
|
|
|
get next() {
|
|
handleResumed();
|
|
return Debugger.stepOver();
|
|
},
|
|
|
|
get step() {
|
|
handleResumed();
|
|
return Debugger.stepInto();
|
|
},
|
|
|
|
get out() {
|
|
handleResumed();
|
|
return Debugger.stepOut();
|
|
},
|
|
|
|
get pause() {
|
|
return Debugger.pause();
|
|
},
|
|
|
|
get backtrace() {
|
|
return currentBacktrace;
|
|
},
|
|
|
|
get breakpoints() {
|
|
return listBreakpoints();
|
|
},
|
|
|
|
exec(expr) {
|
|
return evalInCurrentContext(expr);
|
|
},
|
|
|
|
get profile() {
|
|
return Profiler.start();
|
|
},
|
|
|
|
get profileEnd() {
|
|
return PromisePrototypeThen(Profiler.stop(),
|
|
Profile.createAndRegister);
|
|
},
|
|
|
|
get profiles() {
|
|
return profiles;
|
|
},
|
|
|
|
takeHeapSnapshot(filename = 'node.heapsnapshot') {
|
|
if (heapSnapshotPromise) {
|
|
print(
|
|
'Cannot take heap snapshot because another snapshot is in progress.'
|
|
);
|
|
return heapSnapshotPromise;
|
|
}
|
|
heapSnapshotPromise = new Promise((resolve, reject) => {
|
|
const absoluteFile = Path.resolve(filename);
|
|
const writer = FS.createWriteStream(absoluteFile);
|
|
let sizeWritten = 0;
|
|
function onProgress({ done, total, finished }) {
|
|
if (finished) {
|
|
print('Heap snapshot prepared.');
|
|
} else {
|
|
print(`Heap snapshot: ${done}/${total}`, false);
|
|
}
|
|
}
|
|
|
|
function onChunk({ chunk }) {
|
|
sizeWritten += chunk.length;
|
|
writer.write(chunk);
|
|
print(`Writing snapshot: ${sizeWritten}`, false);
|
|
}
|
|
|
|
function onResolve() {
|
|
writer.end(() => {
|
|
teardown();
|
|
print(`Wrote snapshot: ${absoluteFile}`);
|
|
heapSnapshotPromise = null;
|
|
resolve();
|
|
});
|
|
}
|
|
|
|
function onReject(error) {
|
|
teardown();
|
|
reject(error);
|
|
}
|
|
|
|
function teardown() {
|
|
HeapProfiler.removeListener(
|
|
'reportHeapSnapshotProgress', onProgress);
|
|
HeapProfiler.removeListener('addHeapSnapshotChunk', onChunk);
|
|
}
|
|
|
|
HeapProfiler.on('reportHeapSnapshotProgress', onProgress);
|
|
HeapProfiler.on('addHeapSnapshotChunk', onChunk);
|
|
|
|
print('Heap snapshot: 0/0', false);
|
|
PromisePrototypeThen(
|
|
HeapProfiler.takeHeapSnapshot({ reportProgress: true }),
|
|
onResolve, onReject);
|
|
});
|
|
return heapSnapshotPromise;
|
|
},
|
|
|
|
get watchers() {
|
|
return watchers();
|
|
},
|
|
|
|
watch(expr) {
|
|
ArrayPrototypePush(watchedExpressions, expr);
|
|
},
|
|
|
|
unwatch(expr) {
|
|
const index = ArrayPrototypeIndexOf(watchedExpressions, expr);
|
|
|
|
// Unwatch by expression
|
|
// or
|
|
// Unwatch by watcher number
|
|
ArrayPrototypeSplice(watchedExpressions,
|
|
index !== -1 ? index : +expr, 1);
|
|
},
|
|
|
|
get repl() {
|
|
// Don't display any default messages
|
|
const listeners = ArrayPrototypeSlice(repl.listeners('SIGINT'));
|
|
repl.removeAllListeners('SIGINT');
|
|
|
|
const oldContext = repl.context;
|
|
|
|
exitDebugRepl = () => {
|
|
// Restore all listeners
|
|
process.nextTick(() => {
|
|
ArrayPrototypeForEach(listeners, (listener) => {
|
|
repl.on('SIGINT', listener);
|
|
});
|
|
});
|
|
|
|
// Exit debug repl
|
|
repl.eval = controlEval;
|
|
|
|
// Swap history
|
|
history.debug = repl.history;
|
|
repl.history = history.control;
|
|
|
|
repl.context = oldContext;
|
|
repl.setPrompt('debug> ');
|
|
repl.displayPrompt();
|
|
|
|
repl.removeListener('SIGINT', exitDebugRepl);
|
|
repl.removeListener('exit', exitDebugRepl);
|
|
|
|
exitDebugRepl = null;
|
|
};
|
|
|
|
// Exit debug repl on SIGINT
|
|
repl.on('SIGINT', exitDebugRepl);
|
|
|
|
// Exit debug repl on repl exit
|
|
repl.on('exit', exitDebugRepl);
|
|
|
|
// Set new
|
|
repl.eval = debugEval;
|
|
repl.context = {};
|
|
|
|
// Swap history
|
|
history.control = repl.history;
|
|
repl.history = history.debug;
|
|
|
|
repl.setPrompt('> ');
|
|
|
|
print('Press Ctrl+C to leave debug repl');
|
|
return repl.displayPrompt();
|
|
},
|
|
|
|
get version() {
|
|
return PromisePrototypeThen(Runtime.evaluate({
|
|
expression: 'process.versions.v8',
|
|
contextId: 1,
|
|
returnByValue: true,
|
|
}), ({ result }) => {
|
|
print(result.value);
|
|
});
|
|
},
|
|
|
|
scripts: listScripts,
|
|
|
|
setBreakpoint,
|
|
clearBreakpoint,
|
|
setPauseOnExceptions,
|
|
get breakOnException() {
|
|
return setPauseOnExceptions('all');
|
|
},
|
|
get breakOnUncaught() {
|
|
return setPauseOnExceptions('uncaught');
|
|
},
|
|
get breakOnNone() {
|
|
return setPauseOnExceptions('none');
|
|
},
|
|
|
|
list,
|
|
});
|
|
aliasProperties(context, SHORTCUTS);
|
|
}
|
|
|
|
async function initAfterStart() {
|
|
await Runtime.enable();
|
|
await Profiler.enable();
|
|
await Profiler.setSamplingInterval({ interval: 100 });
|
|
await Debugger.enable();
|
|
await Debugger.setAsyncCallStackDepth({ maxDepth: 0 });
|
|
await Debugger.setBlackboxPatterns({ patterns: [] });
|
|
await Debugger.setPauseOnExceptions({ state: pauseOnExceptionState });
|
|
await restoreBreakpoints();
|
|
return Runtime.runIfWaitingForDebugger();
|
|
}
|
|
|
|
return async function startRepl() {
|
|
inspector.client.on('close', () => {
|
|
resetOnStart();
|
|
});
|
|
inspector.client.on('ready', () => {
|
|
initAfterStart();
|
|
});
|
|
|
|
// Init once for the initial connection
|
|
await initAfterStart();
|
|
|
|
const replOptions = {
|
|
prompt: 'debug> ',
|
|
input: inspector.stdin,
|
|
output: inspector.stdout,
|
|
eval: controlEval,
|
|
useGlobal: false,
|
|
ignoreUndefined: true,
|
|
};
|
|
|
|
repl = Repl.start(replOptions);
|
|
initializeContext(repl.context);
|
|
repl.on('reset', initializeContext);
|
|
|
|
repl.defineCommand('interrupt', () => {
|
|
// We want this for testing purposes where sending Ctrl+C can be tricky.
|
|
repl.emit('SIGINT');
|
|
});
|
|
|
|
return repl;
|
|
};
|
|
}
|
|
module.exports = createRepl;
|