trepan-ni
Version:
Trepan debugger for Node Inspect (and based on node inspect debugger)
1,253 lines (1,092 loc) • 36.7 kB
JavaScript
/*
* 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.
*/
;
const FS = require('fs');
const Path = require('path');
const Repl = require('repl');
const util = require('util');
const vm = require('vm');
const cmds = require('./cmds');
const debuglog = util.debuglog('inspect');
const Intf = require('./interface');
// const CH = require('consolehighlighter');
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);
// Colorize 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;
}
function trimWhitespace(cmd) {
var trimmer = /^\s*(.+)\s*$/m,
matches = trimmer.exec(cmd);
if (matches && matches.length === 2) {
return matches[1];
}
return '';
}
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 createRepl(inspector) {
const { Debugger, HeapProfiler, Profiler, Runtime } = inspector;
let repl; // eslint-disable-line prefer-const
var intf = Intf.intf(
{
termHighlight: true,
useColors: true,
runtime: Runtime,
autoEval: true,
});
intf.convertResultToError = convertResultToError;
// FIXME: Causes circular access. But gives a way to access intf
// inside repl
Debugger.intf = intf;
// Things we want to keep around
const history = { control: [], debug: [] };
const watchedExpressions = [];
let pauseOnExceptionState = 'none';
let lastCommand;
// let tokens;
// let cmdArgs;
let cmdName;
let cmd;
// Things we need to reset when the app restarts
let exitDebugRepl;
// number of lines on either side of "list" line. May want to allow
// adjusting
const listDelta = 5;
intf.Debugger = Debugger;
intf.RemoteObject = RemoteObject;
intf.inspector = inspector;
function isPaused() {
if (!intf.selectedFrame) {
return false;
}
return true;
}
intf.isPaused = isPaused;
function needsPaused() {
if (!isPaused()) {
throw new Error('Requires execution to be paused');
}
}
function resetOnStart() {
intf.knownScripts = {};
intf.currentBacktrace = null;
intf.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() {
needsPaused();
return intf.selectedFrame.location;
}
function isCurrentScript(script) {
return isPaused() && getCurrentLocation().scriptId === script.scriptId;
}
function formatScripts(displayNatives = false) {
function isVisible(script) {
if (displayNatives) return true;
return !script.isNative || isCurrentScript(script);
}
return Object.keys(intf.knownScripts)
.map((scriptId) => intf.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) {
intf.section('current scriptId filename');
print(formatScripts(displayNatives));
}
intf.listScripts = listScripts;
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, stoppedLoc = null) {
Object.assign(this, location);
this.scriptSource = scriptSource;
this.delta = delta;
this.stoppedLoc = stoppedLoc;
}
[util.inspect.custom](depth, options) {
const { scriptId, lineNumber, columnNumber, delta, scriptSource,
stoppedLoc } = 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 = (stoppedLoc &&
i === (stoppedLoc.lineNumber + 1));
const markedLine = isCurrent
? markSourceColumn(lineText, columnNumber, options.colors)
: lineText;
let isBreakpoint = false;
intf.knownBreakpoints.forEach(({ location }) => {
if (!location) return;
if (scriptId === location.scriptId &&
i === (location.lineNumber + 1)) {
isBreakpoint = true;
}
});
let prefixStr;
if (isBreakpoint) {
prefixStr = '*';
} else {
prefixStr = ' ';
}
if (isCurrent) {
prefixStr += '->';
} else {
prefixStr += ' ';
}
return `${leftPad(i, prefixStr, end)} ${markedLine}`;
}).join('\n');
}
}
function getSourceSnippet(location, delta = listDelta) {
const { scriptId } = location;
return Debugger.getScriptSource({ scriptId })
.then(({ scriptSource }) =>
new SourceSnippet(location, delta, scriptSource,
getCurrentLocation()));
}
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));
})
);
}
// Source-code listing starting at *startLine* with *delta* surrounding
// lines.
list(startLine = 0, delta = 5) {
const location = Object.assign({}, this.location);
if (startLine) {
location.lineNumber = startLine - 1;
location.columnNumber = 0;
}
return getSourceSnippet(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 = intf.knownScripts[scriptId];
const relativeUrl =
(script && getRelativePath(script.url)) || '<unknown>';
const frameLocation =
`${relativeUrl}:${lineNumber + 1}:${columnNumber}`;
const prefix = (idx == intf.selectedFrameIndex) ?
'->' : '##';
return `${prefix} ${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') {
needsPaused();
return intf.selectedFrame.loadScopes().then((scopes) => {
return scopes.map((scope) => scope.completionGroup);
});
}
if (intf.selectedFrame) {
return Debugger.evaluateOnCallFrame({
callFrameId: intf.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);
}
// Used for debugger's commands evaluation and execution
function controlEval(input, context, filename, callback) {
debuglog('eval:', input);
function returnToCallback(error, result) {
debuglog('end-eval:', input, error);
callback(error, result);
}
try {
// FIXME: Put this in prepareControlCode(input)
// tokens = input.split(/[ \t\n]+/);
// cmdArgs = tokens.slice(1);
cmdName = trimWhitespace(input).split(/[ (]/)[0];
if (cmdName in intf.aliases) {
cmd = intf.aliases[cmdName];
input = input.replace(cmdName, cmd.name);
} else if (cmdName in intf.commands) {
cmd = intf.commands[cmdName];
} else if (cmdName !== '') {
cmd = null;
}
if (cmd && cmd.paused && !isPaused()) {
print(`This command "${cmdName}" requires program to be paused.`);
returnToCallback(null);
return;
}
input = input.replace(/\n$/, '');
if (!cmd && intf.autoEval === true) {
input = `exec('${input}')`;
} else if (!cmd && intf.autoEval !== 'js') {
print(`Command "${cmdName}" not a debugger command.`);
returnToCallback(null);
return;
} else {
const match = input.match(/^\s*exec\s+([^\n]*)\n/);
if (match) {
input = `exec(${JSON.stringify(match[1])})`;
} else if (intf.autoEval !== 'js') {
// A hack so that command:
// a b -> a(b),
// a b c -> a(b, c)
// a b, c -> a(b, c)
const parts = input.split(/[ \t]+/);
if (parts.length >= 2) {
const part1 = trimWhitespace(parts[0]);
const partn = trimWhitespace(parts[parts.length - 1]);
if (!parts[1] != '(' &&
partn[partn.length - 1] != ')') {
const middle = parts.slice(1)
.map((part, idx) => {
return part.replace(/,$/, '');
});
input = `${part1}(${middle})`;
}
} else if (input === '') {
input = lastCommand;
} else if (input.length > 0 && !input.match(/\(.*\)/)) {
input += '()';
}
}
}
intf.runtime = Runtime;
const result = vm.runInContext(input, context, filename);
lastCommand = input;
if (result && typeof result.then === 'function') {
toCallback(result, returnToCallback);
return;
}
returnToCallback(null, result);
} catch (e) {
returnToCallback(e);
}
}
// Used for debugger's remote evaluation: shell and eval() debugger commands
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 "delta" lines of source code,
function setPauseOnExceptions(state) {
return Debugger.setPauseOnExceptions({ state })
.then(() => {
pauseOnExceptionState = state;
});
}
// Delete a breakpoint
function deleteBreakpoint(findId) {
const breakpoint = intf.knownBreakpoints.find(({ bpNum }) => {
return (
findId === bpNum
);
});
if (!breakpoint) {
print(`Could not find breakpoint ${findId}`);
return Promise.resolve();
}
return Debugger.removeBreakpoint({ breakpointId: breakpoint.breakpointId })
.then(() => {
const idx = intf.knownBreakpoints.indexOf(breakpoint);
intf.knownBreakpoints.splice(idx, 1);
print(`Breakpoint ${findId} deleted`);
});
}
intf.deleteBreakpoint = deleteBreakpoint;
function clearBreakpoint(url, line) {
const breakpoint = intf.knownBreakpoints.find(({ location }) => {
if (!location) return false;
const script = intf.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 = intf.knownBreakpoints.indexOf(breakpoint);
print(`Breakpoint ${intf.knownBreakpoints[idx].bpNum} deleted`);
intf.knownBreakpoints.splice(idx, 1);
});
}
intf.clearBreakpoint = clearBreakpoint;
function restoreBreakpoints() {
const lastBreakpoints = this.knownBreakpoints.slice();
this.knownBreakpoints.length = 0;
this.bpNum = 1;
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.`);
});
}
intf.restoreBreakpoints = restoreBreakpoints;
function handleBreakpointResolved({ breakpointId, location, condition }) {
const script = intf.knownScripts[location.scriptId];
const scriptUrl = script && script.url;
if (scriptUrl) {
Object.assign(location, { scriptUrl });
}
const isExisting = intf.knownBreakpoints.some((bp) => {
if (bp.bpNum === intf.bpNum) {
Object.assign(bp, { location, condition });
return true;
}
return false;
});
if (!isExisting) {
intf.knownBreakpoints.push({
breakpointId,
location,
bpNum: intf.bpNum,
condition
});
}
}
function setBreakpoint(script, line, condition, silent) {
function registerBreakpoint({ breakpointId, actualLocation }) {
handleBreakpointResolved({ breakpointId, location: actualLocation,
condition });
if (actualLocation && actualLocation.scriptId) {
if (!silent) {
print(`Breakpoint ${intf.bpNum} set in file \
${actualLocation.scriptUrl}, line ${actualLocation.lineNumber + 1}.`);
intf.bpNum++;
return getSourceSnippet(actualLocation, 5);
}
intf.bpNum++;
} 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 = intf.selectedFrame
? Debugger.evaluateOnCallFrame({
callFrameId: intf.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 (intf.knownScripts[script]) {
scriptId = script;
} else {
for (const id of Object.keys(intf.knownScripts)) {
const scriptUrl = intf.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 escapedPath = script.replace(
new RegExp('([/\\.?*()^${}|[]])', '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);
});
}
intf.setBreakpoint = setBreakpoint;
Debugger.on('paused', ({ callFrames, reason /* , hitBreakpoints */ }) => {
// Save execution context's data
intf.currentBacktrace = Backtrace.from(callFrames);
intf.selectedFrame = intf.currentBacktrace[0];
intf.breakType = reason === 'other' ? 'break' : reason;
const { scriptId, lineNumber } = intf.selectedFrame.location;
const script = intf.knownScripts[scriptId];
const scriptUrl = script ? getRelativePath(script.url) : '[unknown]';
const header = `${intf.breakType} in ${scriptUrl}:${lineNumber + 1}`;
inspector.suspendReplWhile(() =>
Promise.all([formatWatchers(true), intf.selectedFrame.list(0, 2)])
.then(([watcherList, context]) => {
if (watcherList) {
return `${watcherList}\n${inspect(context)}`;
}
return inspect(context);
}).then((breakContext) => {
print(`${header}\n${breakContext}`);
}));
});
function handleResumed() {
intf.currentBacktrace = null;
intf.selectedFrame = null;
}
intf.handleResumed = handleResumed;
Debugger.on('resumed', handleResumed);
Debugger.on('breakpointResolved', handleBreakpointResolved);
Debugger.on('scriptParsed', (script) => {
const { scriptId, url } = script;
if (url) {
intf.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,
});
});
// Note: debugger commands can't conflict with JavaScript
// reserved words. In particular, this prohibits the use of
// 'eval', and 'continue'.
// However these can be aliases for other commands.
/*=================================
Debugger 'exec' command
===================================*/
intf.defineCommand('exec', repl, {
paused: false,
help: `Evaluate the expression and print the value
Usage: eval
`,
aliases: ['e', 'eval'],
run: function(expr) {
if (repl.state !== 'connected') {
print('Program not connected; Use `run` to start the app again.');
} else {
return evalInCurrentContext(expr);
}
}
});
/*=================================
Debugger 'heapSnapshot'
===================================*/
intf.defineCommand('heapSnapshot', repl, {
connection: true,
help: `Take a heap snapshot and save to disk as JSON.
Usage: heapSnapshot(filepath = 'node.heapsnapshot')
`,
aliases: [],
run: function(filename = 'node.heapsnapshot') {
return 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 snaphost prepared.');
} else {
print(`Heap snapshot: ${done}/${total}`, true);
}
}
function onChunk({ chunk }) {
sizeWritten += chunk.length;
writer.write(chunk);
print(`Writing snapshot: ${sizeWritten}`, true);
}
function onResolve() {
writer.end(() => {
teardown();
print(`Wrote snapshot: ${absoluteFile}`);
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', true);
HeapProfiler.takeHeapSnapshot({ reportProgress: true })
.then(onResolve, onReject);
});
}
});
/*=================================
Debugger 'breakOnException' command
===================================*/
intf.defineCommand('breakOnException', repl, {
connection: true,
help: `Pause execution whenever an exception is thrown.
`,
aliases: [],
run: function() {
return setPauseOnExceptions('all');
}
});
/*=================================
Debugger 'breakOnNone' command
===================================*/
intf.defineCommand('breakOnNone', repl, {
connection: true,
help: `Pause execution whenever an exception is thrown.
`,
aliases: [],
run: function() {
return setPauseOnExceptions('none');
}
});
/*=================================
Debugger 'breakOnUncaught' command
===================================*/
intf.defineCommand('breakOnUncaught', repl, {
connection: true,
help: `Pause execution whenever an exception is thrown.
`,
aliases: [],
run: function() {
return setPauseOnExceptions('uncaught');
}
});
/*=================================
Debugger 'profile' command
===================================*/
intf.defineCommand('profile', repl, {
connection: true,
help: `Start CPU profiling session.
Usage: profile
See also:
---------
profileEnd, profiles
`,
aliases: [],
run: function() {
return Profiler.start();
}
});
/*=================================
Debugger 'profileEnd' command
===================================*/
intf.defineCommand('profileEnd', repl, {
connection: true,
help: `Stop current CPU profiling session.
Usage: profileEnd
See also:
---------
profile, profiles
`,
aliases: [],
run: function() {
return Profiler.stop()
.then(Profile.createAndRegister);
}
});
/*=================================
Debugger 'profiles' command
===================================*/
intf.defineCommand('profiles', repl, {
connection: true,
help: `Show array of completed CPU profiling sessions.
Usage: profiles
See also:
---------
profile, profileEnd
`,
aliases: [],
run: function() {
return profiles;
}
});
/*=================================
Debugger 'shell' command
===================================*/
intf.defineCommand('shell', repl, {
help: `Enter a debug repl that works like exec
Usage: shell
`,
aliases: ['repl'],
run: function() {
// Don't display any default messages
const listeners = repl.rli.listeners('SIGINT').slice(0);
repl.rli.removeAllListeners('SIGINT');
const oldContext = repl.context;
// console.log(`${Object.keys(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('(trepan-ni) ');
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();
}
});
/*=================================
Debugger 'unwatch' command
===================================*/
intf.defineCommand('unwatch', repl, {
connection: true,
help: `Stop watching an expression
Usage: unwatch(expr)
`,
aliases: [],
run: function(expr) {
const index = watchedExpressions.indexOf(expr);
// Unwatch by expression
// or
// Unwatch by watcher number
watchedExpressions.splice(index !== -1 ? index : +expr, 1);
}
});
/*=================================
Debugger 'watch' command
===================================*/
intf.defineCommand('watch', repl, {
connection: false,
help: `Start watching an expression.
Usage: watch(expr)
`,
aliases: [],
run: function(expr) {
watchedExpressions.push(expr);
}
});
/*=================================
Debugger 'watchers' command
===================================*/
intf.defineCommand('watchers', repl, {
connection: false,
help: `Print all watched expressions and their current values
Usage: watchers
`,
aliases: [],
run: watchers,
});
}
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 }),
intf.restoreBreakpoints(),
Runtime.runIfWaitingForDebugger(),
];
return Promise.all(setupTasks);
}
return function startRepl() {
inspector.client.on('close', () => {
resetOnStart();
});
inspector.client.on('ready', () => {
initAfterStart();
});
const replOptions = {
prompt: '(trepan-ni) ',
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);
intf.defineCommand('interrupt', repl, {
connection: false,
help: 'use instead of sending CTRL-C',
aliases: [],
run: function() {
// We want this for testing purposes where sending CTRL-C can be tricky.
repl.rli.emit('SIGINT');
}
});
// Add debugger commands
cmds.defineCommands(intf, repl);
intf.repl = repl;
// Init once for the initial connection
initAfterStart();
return repl;
};
}
module.exports = createRepl;