mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1089 lines
31 KiB
1089 lines
31 KiB
/*
|
|
* Copyright Node.js contributors. All rights reserved.
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to
|
|
* deal in the Software without restriction, including without limitation the
|
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
* sell copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
* IN THE SOFTWARE.
|
|
*/
|
|
'use strict';
|
|
const FS = require('fs');
|
|
const Path = require('path');
|
|
const Repl = require('repl');
|
|
const util = require('util');
|
|
const vm = require('vm');
|
|
|
|
const debuglog = util.debuglog('inspect');
|
|
|
|
const SHORTCUTS = {
|
|
cont: 'c',
|
|
next: 'n',
|
|
step: 's',
|
|
out: 'o',
|
|
backtrace: 'bt',
|
|
setBreakpoint: 'sb',
|
|
clearBreakpoint: 'cb',
|
|
run: 'r',
|
|
};
|
|
|
|
const HELP = `
|
|
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) 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.
|
|
`.trim();
|
|
|
|
const FUNCTION_NAME_PATTERN = /^(?:function\*? )?([^(\s]+)\(/;
|
|
function extractFunctionName(description) {
|
|
const fnNameMatch = description.match(FUNCTION_NAME_PATTERN);
|
|
return fnNameMatch ? `: ${fnNameMatch[1]}` : '';
|
|
}
|
|
|
|
const NATIVES = process.binding('natives');
|
|
function isNativeUrl(url) {
|
|
return url.replace('.js', '') in NATIVES || url === 'bootstrap_node.js';
|
|
}
|
|
|
|
function getRelativePath(filename) {
|
|
const dir = Path.join(Path.resolve(), 'x').slice(0, -1);
|
|
|
|
// Change path to relative, if possible
|
|
if (filename.indexOf(dir) === 0) {
|
|
return filename.slice(dir.length);
|
|
}
|
|
return filename;
|
|
}
|
|
|
|
function toCallback(promise, callback) {
|
|
function forward(...args) {
|
|
process.nextTick(() => callback(...args));
|
|
}
|
|
promise.then(forward.bind(null, null), forward);
|
|
}
|
|
|
|
// 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 = Math.max(2, String(maxN).length) + 1;
|
|
const nspaces = nchars - s.length - 1;
|
|
|
|
return prefix + ' '.repeat(nspaces) + s;
|
|
}
|
|
|
|
function markSourceColumn(sourceText, position, useColors) {
|
|
if (!sourceText) return '';
|
|
|
|
const head = sourceText.slice(0, position);
|
|
let tail = sourceText.slice(position);
|
|
|
|
// Colourize char if stdout supports colours
|
|
if (useColors) {
|
|
tail = tail.replace(/(.+?)([^\w]|$)/, '\u001b[32m$1\u001b[39m$2');
|
|
}
|
|
|
|
// Return source line with coloured char at `position`
|
|
return [head, tail].join('');
|
|
}
|
|
|
|
function extractErrorMessage(stack) {
|
|
if (!stack) return '<unknown>';
|
|
const m = stack.match(/^\w+: ([^\n]+)/);
|
|
return m ? m[1] : stack;
|
|
}
|
|
|
|
function convertResultToError(result) {
|
|
const { className, description } = result;
|
|
const err = new Error(extractErrorMessage(description));
|
|
err.stack = description;
|
|
Object.defineProperty(err, 'name', { value: className });
|
|
return err;
|
|
}
|
|
|
|
class RemoteObject {
|
|
constructor(attributes) {
|
|
Object.assign(this, attributes);
|
|
if (this.type === 'number') {
|
|
this.value =
|
|
this.unserializableValue ? +this.unserializableValue : +this.value;
|
|
}
|
|
}
|
|
|
|
[util.inspect.custom](depth, opts) {
|
|
function formatProperty(prop) {
|
|
switch (prop.type) {
|
|
case 'string':
|
|
case 'undefined':
|
|
return util.inspect(prop.value, opts);
|
|
|
|
case 'number':
|
|
case 'boolean':
|
|
return opts.stylize(prop.value, prop.type);
|
|
|
|
case 'object':
|
|
case 'symbol':
|
|
if (prop.subtype === 'date') {
|
|
return util.inspect(new Date(prop.value), opts);
|
|
}
|
|
if (prop.subtype === 'array') {
|
|
return opts.stylize(prop.value, 'special');
|
|
}
|
|
return opts.stylize(prop.value, prop.subtype || 'special');
|
|
|
|
default:
|
|
return prop.value;
|
|
}
|
|
}
|
|
switch (this.type) {
|
|
case 'boolean':
|
|
case 'number':
|
|
case 'string':
|
|
case 'undefined':
|
|
return util.inspect(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 util.inspect(new Date(this.description), opts);
|
|
|
|
case 'null':
|
|
return util.inspect(null, opts);
|
|
|
|
case 'regexp':
|
|
return opts.stylize(this.description, 'regexp');
|
|
|
|
default:
|
|
break;
|
|
}
|
|
if (this.preview) {
|
|
const props = this.preview.properties
|
|
.map((prop, idx) => {
|
|
const value = formatProperty(prop);
|
|
if (prop.name === `${idx}`) return value;
|
|
return `${prop.name}: ${value}`;
|
|
});
|
|
if (this.preview.overflow) {
|
|
props.push('...');
|
|
}
|
|
const singleLine = props.join(', ');
|
|
const propString =
|
|
singleLine.length > 60 ? props.join(',\n ') : singleLine;
|
|
|
|
return this.subtype === 'array' ?
|
|
`[ ${propString} ]` : `{ ${propString} }`;
|
|
}
|
|
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) {
|
|
Object.assign(this, scope);
|
|
this.properties = new Map(properties.map((prop) => {
|
|
const value = new RemoteObject(prop.value);
|
|
return [prop.name, value];
|
|
}));
|
|
this.completionGroup = properties.map((prop) => prop.name);
|
|
}
|
|
|
|
[util.inspect.custom](depth, opts) {
|
|
const type = `${this.type[0].toUpperCase()}${this.type.slice(1)}`;
|
|
const name = this.name ? `<${this.name}>` : '';
|
|
const prefix = `${type}${name} `;
|
|
return util.inspect(this.properties, opts)
|
|
.replace(/^Map /, prefix);
|
|
}
|
|
}
|
|
|
|
function copyOwnProperties(target, source) {
|
|
Object.getOwnPropertyNames(source).forEach((prop) => {
|
|
const descriptor = Object.getOwnPropertyDescriptor(source, prop);
|
|
Object.defineProperty(target, prop, descriptor);
|
|
});
|
|
}
|
|
|
|
function aliasProperties(target, mapping) {
|
|
Object.keys(mapping).forEach((key) => {
|
|
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
Object.defineProperty(target, mapping[key], descriptor);
|
|
});
|
|
}
|
|
|
|
function createRepl(inspector) {
|
|
const { Debugger, HeapProfiler, Profiler, Runtime } = inspector;
|
|
|
|
let repl; // eslint-disable-line prefer-const
|
|
|
|
// Things we want to keep around
|
|
const history = { control: [], debug: [] };
|
|
const watchedExpressions = [];
|
|
const knownBreakpoints = [];
|
|
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 util.inspect(value, INSPECT_OPTIONS);
|
|
}
|
|
|
|
function print(value, oneline = false) {
|
|
const text = typeof value === 'string' ? value : inspect(value);
|
|
return inspector.print(text, oneline);
|
|
}
|
|
|
|
function getCurrentLocation() {
|
|
if (!selectedFrame) {
|
|
throw new 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 Object.keys(knownScripts)
|
|
.map((scriptId) => knownScripts[scriptId])
|
|
.filter(isVisible)
|
|
.map((script) => {
|
|
const isCurrent = isCurrentScript(script);
|
|
const { isNative, url } = script;
|
|
const name = `${getRelativePath(url)}${isNative ? ' <native>' : ''}`;
|
|
return `${isCurrent ? '*' : ' '} ${script.scriptId}: ${name}`;
|
|
})
|
|
.join('\n');
|
|
}
|
|
function listScripts(displayNatives = false) {
|
|
print(formatScripts(displayNatives));
|
|
}
|
|
listScripts[util.inspect.custom] = function listWithoutInternal() {
|
|
return formatScripts();
|
|
};
|
|
|
|
const profiles = [];
|
|
class Profile {
|
|
constructor(data) {
|
|
this.data = data;
|
|
}
|
|
|
|
static createAndRegister({ profile }) {
|
|
const p = new Profile(profile);
|
|
profiles.push(p);
|
|
return p;
|
|
}
|
|
|
|
[util.inspect.custom](depth, { stylize }) {
|
|
const { startTime, endTime } = this.data;
|
|
return stylize(`[Profile ${endTime - startTime}μs]`, 'special');
|
|
}
|
|
|
|
save(filename = 'node.cpuprofile') {
|
|
const absoluteFile = Path.resolve(filename);
|
|
const json = JSON.stringify(this.data);
|
|
FS.writeFileSync(absoluteFile, json);
|
|
print('Saved profile to ' + absoluteFile);
|
|
}
|
|
}
|
|
|
|
class SourceSnippet {
|
|
constructor(location, delta, scriptSource) {
|
|
Object.assign(this, location);
|
|
this.scriptSource = scriptSource;
|
|
this.delta = delta;
|
|
}
|
|
|
|
[util.inspect.custom](depth, options) {
|
|
const { scriptId, lineNumber, columnNumber, delta, scriptSource } = this;
|
|
const start = Math.max(1, lineNumber - delta + 1);
|
|
const end = lineNumber + delta + 1;
|
|
|
|
const lines = scriptSource.split('\n');
|
|
return lines.slice(start - 1, end).map((lineText, offset) => {
|
|
const i = start + offset;
|
|
const isCurrent = i === (lineNumber + 1);
|
|
|
|
const markedLine = isCurrent
|
|
? markSourceColumn(lineText, columnNumber, options.colors)
|
|
: lineText;
|
|
|
|
let isBreakpoint = false;
|
|
knownBreakpoints.forEach(({ 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}`;
|
|
}).join('\n');
|
|
}
|
|
}
|
|
|
|
function getSourceSnippet(location, delta = 5) {
|
|
const { scriptId } = location;
|
|
return Debugger.getScriptSource({ scriptId })
|
|
.then(({ scriptSource }) =>
|
|
new SourceSnippet(location, delta, scriptSource));
|
|
}
|
|
|
|
class CallFrame {
|
|
constructor(callFrame) {
|
|
Object.assign(this, callFrame);
|
|
}
|
|
|
|
loadScopes() {
|
|
return Promise.all(
|
|
this.scopeChain
|
|
.filter((scope) => scope.type !== 'global')
|
|
.map((scope) => {
|
|
const { objectId } = scope.object;
|
|
return Runtime.getProperties({
|
|
objectId,
|
|
generatePreview: true,
|
|
}).then(({ result }) => new ScopeSnapshot(scope, result));
|
|
})
|
|
);
|
|
}
|
|
|
|
list(delta = 5) {
|
|
return getSourceSnippet(this.location, delta);
|
|
}
|
|
}
|
|
|
|
class Backtrace extends Array {
|
|
[util.inspect.custom]() {
|
|
return this.map((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}`;
|
|
}).join('\n');
|
|
}
|
|
|
|
static from(callFrames) {
|
|
return super.from(Array.from(callFrames).map((callFrame) => {
|
|
if (callFrame instanceof CallFrame) {
|
|
return callFrame;
|
|
}
|
|
return new CallFrame(callFrame);
|
|
}));
|
|
}
|
|
}
|
|
|
|
function prepareControlCode(input) {
|
|
if (input === '\n') return lastCommand;
|
|
// exec process.title => exec("process.title");
|
|
const match = input.match(/^\s*exec\s+([^\n]*)/);
|
|
if (match) {
|
|
lastCommand = `exec(${JSON.stringify(match[1])})`;
|
|
} else {
|
|
lastCommand = input;
|
|
}
|
|
return lastCommand;
|
|
}
|
|
|
|
function evalInCurrentContext(code) {
|
|
// Repl asked for scope variables
|
|
if (code === '.scope') {
|
|
if (!selectedFrame) {
|
|
return Promise.reject(new Error('Requires execution to be paused'));
|
|
}
|
|
return selectedFrame.loadScopes().then((scopes) => {
|
|
return scopes.map((scope) => scope.completionGroup);
|
|
});
|
|
}
|
|
|
|
if (selectedFrame) {
|
|
return Debugger.evaluateOnCallFrame({
|
|
callFrameId: selectedFrame.callFrameId,
|
|
expression: code,
|
|
objectGroup: 'node-inspect',
|
|
generatePreview: true,
|
|
}).then(RemoteObject.fromEvalResult);
|
|
}
|
|
return Runtime.evaluate({
|
|
expression: code,
|
|
objectGroup: 'node-inspect',
|
|
generatePreview: true,
|
|
}).then(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);
|
|
|
|
if (result && typeof result.then === 'function') {
|
|
toCallback(result, returnToCallback);
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
|
|
try {
|
|
const result = evalInCurrentContext(input);
|
|
|
|
if (result && typeof result.then === 'function') {
|
|
toCallback(result, returnToCallback);
|
|
return;
|
|
}
|
|
returnToCallback(null, result);
|
|
} catch (e) {
|
|
returnToCallback(e);
|
|
}
|
|
}
|
|
|
|
function formatWatchers(verbose = false) {
|
|
if (!watchedExpressions.length) {
|
|
return Promise.resolve('');
|
|
}
|
|
|
|
const inspectValue = (expr) =>
|
|
evalInCurrentContext(expr)
|
|
// .then(formatValue)
|
|
.catch((error) => `<${error.message}>`);
|
|
const lastIndex = watchedExpressions.length - 1;
|
|
|
|
return Promise.all(watchedExpressions.map(inspectValue))
|
|
.then((values) => {
|
|
const lines = watchedExpressions
|
|
.map((expr, idx) => {
|
|
const prefix = `${leftPad(idx, ' ', lastIndex)}: ${expr} =`;
|
|
const value = inspect(values[idx], { colors: true });
|
|
if (value.indexOf('\n') === -1) {
|
|
return `${prefix} ${value}`;
|
|
}
|
|
return `${prefix}\n ${value.split('\n').join('\n ')}`;
|
|
});
|
|
return lines.join('\n');
|
|
})
|
|
.then((valueList) => {
|
|
return verbose ? `Watchers:\n${valueList}\n` : valueList;
|
|
});
|
|
}
|
|
|
|
function watchers(verbose = false) {
|
|
return formatWatchers(verbose).then(print);
|
|
}
|
|
|
|
// List source code
|
|
function list(delta = 5) {
|
|
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) {
|
|
Object.assign(location, { scriptUrl });
|
|
}
|
|
const isExisting = knownBreakpoints.some((bp) => {
|
|
if (bp.breakpointId === breakpointId) {
|
|
Object.assign(bp, { location });
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
if (!isExisting) {
|
|
knownBreakpoints.push({ 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 = knownBreakpoints
|
|
.map((bp, idx) => `#${idx} ${formatLocation(bp.location)}`)
|
|
.join('\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 Debugger
|
|
.setBreakpoint({ location: getCurrentLocation(), condition })
|
|
.then(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 Debugger.setBreakpoint({ location, condition })
|
|
.then(registerBreakpoint);
|
|
}
|
|
|
|
if (typeof script !== 'string') {
|
|
throw new TypeError(`setBreakpoint() expects a string, got ${script}`);
|
|
}
|
|
|
|
// setBreakpoint('fn()'): Break when a function is called
|
|
if (script.endsWith('()')) {
|
|
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 debugCall.then(({ 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 {
|
|
for (const id of Object.keys(knownScripts)) {
|
|
const scriptUrl = knownScripts[id].url;
|
|
if (scriptUrl && scriptUrl.indexOf(script) !== -1) {
|
|
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 Debugger.setBreakpoint({ location, condition })
|
|
.then(registerBreakpoint);
|
|
}
|
|
|
|
const escapedPath = script.replace(/([/\\.?*()^${}|[\]])/g, '\\$1');
|
|
const urlRegex = `^(.*[\\/\\\\])?${escapedPath}$`;
|
|
|
|
return Debugger
|
|
.setBreakpointByUrl({ urlRegex, lineNumber: line - 1, condition })
|
|
.then((bp) => {
|
|
// TODO: handle bp.locations in case the regex matches existing files
|
|
if (!bp.location) { // Fake it for now.
|
|
Object.assign(bp, {
|
|
actualLocation: {
|
|
scriptUrl: `.*/${script}$`,
|
|
lineNumber: line - 1,
|
|
},
|
|
});
|
|
}
|
|
return registerBreakpoint(bp);
|
|
});
|
|
}
|
|
|
|
function clearBreakpoint(url, line) {
|
|
const breakpoint = knownBreakpoints.find(({ location }) => {
|
|
if (!location) return false;
|
|
const script = knownScripts[location.scriptId];
|
|
if (!script) return false;
|
|
return (
|
|
script.url.indexOf(url) !== -1 && (location.lineNumber + 1) === line
|
|
);
|
|
});
|
|
if (!breakpoint) {
|
|
print(`Could not find breakpoint at ${url}:${line}`);
|
|
return Promise.resolve();
|
|
}
|
|
return Debugger.removeBreakpoint({ breakpointId: breakpoint.breakpointId })
|
|
.then(() => {
|
|
const idx = knownBreakpoints.indexOf(breakpoint);
|
|
knownBreakpoints.splice(idx, 1);
|
|
});
|
|
}
|
|
|
|
function restoreBreakpoints() {
|
|
const lastBreakpoints = knownBreakpoints.slice();
|
|
knownBreakpoints.length = 0;
|
|
const newBreakpoints = lastBreakpoints
|
|
.filter(({ location }) => !!location.scriptUrl)
|
|
.map(({ location }) =>
|
|
setBreakpoint(location.scriptUrl, location.lineNumber + 1));
|
|
if (!newBreakpoints.length) return Promise.resolve();
|
|
return Promise.all(newBreakpoints).then((results) => {
|
|
print(`${results.length} breakpoints restored.`);
|
|
});
|
|
}
|
|
|
|
function setPauseOnExceptions(state) {
|
|
return Debugger.setPauseOnExceptions({ state })
|
|
.then(() => {
|
|
pauseOnExceptionState = state;
|
|
});
|
|
}
|
|
|
|
Debugger.on('paused', ({ callFrames, reason /* , hitBreakpoints */ }) => {
|
|
// 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(() =>
|
|
Promise.all([formatWatchers(true), selectedFrame.list(2)])
|
|
.then(([watcherList, context]) => {
|
|
if (watcherList) {
|
|
return `${watcherList}\n${inspect(context)}`;
|
|
}
|
|
return inspect(context);
|
|
}).then((breakContext) => {
|
|
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] = Object.assign({
|
|
isNative: isNativeUrl(url),
|
|
}, script);
|
|
}
|
|
});
|
|
|
|
Profiler.on('consoleProfileFinished', ({ profile }) => {
|
|
Profile.createAndRegister({ profile });
|
|
print([
|
|
'Captured new CPU profile.',
|
|
`Access it with profiles[${profiles.length - 1}]`
|
|
].join('\n'));
|
|
});
|
|
|
|
function initializeContext(context) {
|
|
inspector.domainNames.forEach((domain) => {
|
|
Object.defineProperty(context, domain, {
|
|
value: inspector[domain],
|
|
enumerable: true,
|
|
configurable: true,
|
|
writeable: false,
|
|
});
|
|
});
|
|
|
|
copyOwnProperties(context, {
|
|
get help() {
|
|
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 Profiler.stop()
|
|
.then(Profile.createAndRegister);
|
|
},
|
|
|
|
get profiles() {
|
|
return profiles;
|
|
},
|
|
|
|
takeHeapSnapshot(filename = 'node.heapsnapshot') {
|
|
return new Promise((resolve, reject) => {
|
|
const absoluteFile = Path.resolve(filename);
|
|
const writer = FS.createWriteStream(absoluteFile);
|
|
let totalSize;
|
|
let sizeWritten = 0;
|
|
function onProgress({ done, total, finished }) {
|
|
totalSize = total;
|
|
if (finished) {
|
|
print('Heap snaphost prepared.');
|
|
} else {
|
|
print(`Heap snapshot: ${done}/${total}`, true);
|
|
}
|
|
}
|
|
function onChunk({ chunk }) {
|
|
sizeWritten += chunk.length;
|
|
writer.write(chunk);
|
|
print(`Writing snapshot: ${sizeWritten}/${totalSize}`, true);
|
|
if (sizeWritten >= totalSize) {
|
|
writer.end();
|
|
teardown();
|
|
print(`Wrote snapshot: ${absoluteFile}`);
|
|
resolve();
|
|
}
|
|
}
|
|
function teardown() {
|
|
HeapProfiler.removeListener(
|
|
'reportHeapSnapshotProgress', onProgress);
|
|
HeapProfiler.removeListener('addHeapSnapshotChunk', onChunk);
|
|
}
|
|
|
|
HeapProfiler.on('reportHeapSnapshotProgress', onProgress);
|
|
HeapProfiler.on('addHeapSnapshotChunk', onChunk);
|
|
|
|
print('Heap snapshot: 0/0', true);
|
|
HeapProfiler.takeHeapSnapshot({ reportProgress: true })
|
|
.then(null, (error) => {
|
|
teardown();
|
|
reject(error);
|
|
});
|
|
});
|
|
},
|
|
|
|
get watchers() {
|
|
return watchers();
|
|
},
|
|
|
|
watch(expr) {
|
|
watchedExpressions.push(expr);
|
|
},
|
|
|
|
unwatch(expr) {
|
|
const index = watchedExpressions.indexOf(expr);
|
|
|
|
// Unwatch by expression
|
|
// or
|
|
// Unwatch by watcher number
|
|
watchedExpressions.splice(index !== -1 ? index : +expr, 1);
|
|
},
|
|
|
|
get repl() {
|
|
// Don't display any default messages
|
|
const listeners = repl.rli.listeners('SIGINT').slice(0);
|
|
repl.rli.removeAllListeners('SIGINT');
|
|
|
|
const oldContext = repl.context;
|
|
|
|
exitDebugRepl = () => {
|
|
// Restore all listeners
|
|
process.nextTick(() => {
|
|
listeners.forEach((listener) => {
|
|
repl.rli.on('SIGINT', listener);
|
|
});
|
|
});
|
|
|
|
// Exit debug repl
|
|
repl.eval = controlEval;
|
|
|
|
// Swap history
|
|
history.debug = repl.rli.history;
|
|
repl.rli.history = history.control;
|
|
|
|
repl.context = oldContext;
|
|
repl.rli.setPrompt('debug> ');
|
|
repl.displayPrompt();
|
|
|
|
repl.rli.removeListener('SIGINT', exitDebugRepl);
|
|
repl.removeListener('exit', exitDebugRepl);
|
|
|
|
exitDebugRepl = null;
|
|
};
|
|
|
|
// Exit debug repl on SIGINT
|
|
repl.rli.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.rli.history;
|
|
repl.rli.history = history.debug;
|
|
|
|
repl.rli.setPrompt('> ');
|
|
|
|
print('Press Ctrl + C to leave debug repl');
|
|
repl.displayPrompt();
|
|
},
|
|
|
|
get version() {
|
|
return Runtime.evaluate({
|
|
expression: 'process.versions.v8',
|
|
contextId: 1,
|
|
returnByValue: true,
|
|
}).then(({ 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);
|
|
}
|
|
|
|
function initAfterStart() {
|
|
const setupTasks = [
|
|
Runtime.enable(),
|
|
Profiler.enable(),
|
|
Profiler.setSamplingInterval({ interval: 100 }),
|
|
Debugger.enable(),
|
|
Debugger.setPauseOnExceptions({ state: 'none' }),
|
|
Debugger.setAsyncCallStackDepth({ maxDepth: 0 }),
|
|
Debugger.setBlackboxPatterns({ patterns: [] }),
|
|
Debugger.setPauseOnExceptions({ state: pauseOnExceptionState }),
|
|
restoreBreakpoints(),
|
|
Runtime.runIfWaitingForDebugger(),
|
|
];
|
|
return Promise.all(setupTasks);
|
|
}
|
|
|
|
return function startRepl() {
|
|
inspector.client.on('close', () => {
|
|
resetOnStart();
|
|
});
|
|
inspector.client.on('ready', () => {
|
|
initAfterStart();
|
|
});
|
|
|
|
const replOptions = {
|
|
prompt: 'debug> ',
|
|
input: inspector.stdin,
|
|
output: inspector.stdout,
|
|
eval: controlEval,
|
|
useGlobal: false,
|
|
ignoreUndefined: true,
|
|
};
|
|
|
|
repl = Repl.start(replOptions); // eslint-disable-line prefer-const
|
|
initializeContext(repl.context);
|
|
repl.on('reset', initializeContext);
|
|
|
|
repl.defineCommand('interrupt', () => {
|
|
// We want this for testing purposes where sending CTRL-C can be tricky.
|
|
repl.rli.emit('SIGINT');
|
|
});
|
|
|
|
// Init once for the initial connection
|
|
initAfterStart();
|
|
|
|
return repl;
|
|
};
|
|
}
|
|
module.exports = createRepl;
|
|
|