Skip to content
This repository was archived by the owner on Feb 1, 2022. It is now read-only.

Commit 23c62f8

Browse files
author
Jan Krems
committed
feat: Print break context
1 parent 7d20b7d commit 23c62f8

3 files changed

Lines changed: 156 additions & 2 deletions

File tree

lib/internal/inspect-repl.js

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,69 @@
2020
* IN THE SOFTWARE.
2121
*/
2222
'use strict';
23+
const Path = require('path');
2324
const Repl = require('repl');
2425
const vm = require('vm');
2526

27+
const NATIVES = process.binding('natives');
28+
29+
function getRelativePath(filename) {
30+
const dir = `${Path.resolve()}/`;
31+
32+
// Change path to relative, if possible
33+
if (filename.indexOf(dir) === 0) {
34+
return filename.slice(dir.length);
35+
}
36+
return filename;
37+
}
38+
2639
function toCallback(promise, callback) {
2740
function forward(...args) {
2841
process.nextTick(() => callback(...args));
2942
}
3043
promise.then(forward.bind(null, null), forward);
3144
}
3245

46+
// Adds spaces and prefix to number
47+
// maxN is a maximum number we should have space for
48+
function leftPad(n, prefix, maxN) {
49+
const s = n.toString();
50+
const nchars = Math.max(2, String(maxN).length) + 1;
51+
const nspaces = nchars - s.length - 1;
52+
53+
return prefix + ' '.repeat(nspaces) + s;
54+
}
55+
56+
function markSourceColumn(sourceText, position, repl) {
57+
if (!sourceText) return '';
58+
59+
const head = sourceText.slice(0, position);
60+
let tail = sourceText.slice(position);
61+
62+
// Colourize char if stdout supports colours
63+
if (repl.useColors) {
64+
tail = tail.replace(/(.+?)([^\w]|$)/, '\u001b[32m$1\u001b[39m$2');
65+
}
66+
67+
// Return source line with coloured char at `position`
68+
return [head, tail].join('');
69+
}
70+
3371
function startRepl(inspector) {
34-
let repl; // forward declaration
72+
const { Debugger } = inspector;
73+
74+
let repl; // eslint-disable-line prefer-const
3575
let lastCommand;
3676

77+
const knownScripts = {};
78+
const watchedExpressions = [];
79+
const knownBreakpoints = [];
80+
81+
// let currentBacktrace;
82+
let selectedFrame;
83+
84+
const print = inspector.print.bind(inspector);
85+
3786
function prepareControlCode(input) {
3887
if (input === '\n') return lastCommand;
3988
// exec process.title => exec("process.title");
@@ -61,6 +110,73 @@ function startRepl(inspector) {
61110
}
62111
}
63112

113+
function watchers() {
114+
return Promise.resolve(watchedExpressions);
115+
}
116+
117+
// List source code
118+
function list(delta = 5) {
119+
const { scriptId, lineNumber, columnNumber } = selectedFrame.location;
120+
const start = Math.max(1, lineNumber - delta + 1);
121+
const end = lineNumber + delta + 1;
122+
123+
return Debugger.getScriptSource({ scriptId })
124+
.then(({ scriptSource }) => {
125+
const lines = scriptSource.split('\n');
126+
for (let i = start; i <= lines.length && i <= end; ++i) {
127+
const isCurrent = i === (lineNumber + 1);
128+
129+
let lineText = lines[i - 1];
130+
if (isCurrent) {
131+
lineText = markSourceColumn(lineText, columnNumber, inspector.repl);
132+
}
133+
134+
let isBreakpoint = false;
135+
knownBreakpoints.forEach(({ location }) => {
136+
if (location && location.scriptId === scriptId && (location.lineNumber + 1) === i) {
137+
isBreakpoint = true;
138+
}
139+
});
140+
141+
let prefixChar = ' ';
142+
if (isCurrent) {
143+
prefixChar = '>';
144+
} else if (isBreakpoint) {
145+
prefixChar = '*';
146+
}
147+
print(`${leftPad(i, prefixChar, end)} ${lineText}`);
148+
}
149+
})
150+
.then(null, error => {
151+
print('You can\'t list source code right now');
152+
throw error;
153+
});
154+
}
155+
156+
Debugger.on('paused', ({ callFrames, reason /* , hitBreakpoints */ }) => {
157+
// Save execution context's data
158+
// currentBacktrace = callFrames;
159+
selectedFrame = callFrames[0];
160+
const { scriptId, lineNumber } = selectedFrame.location;
161+
162+
const script = knownScripts[scriptId];
163+
const scriptUrl = script ? getRelativePath(script.url) : '[unknown]';
164+
print(`${reason === 'other' ? 'break' : reason} in ${scriptUrl}:${lineNumber + 1}`);
165+
166+
inspector.suspendReplWhile(() =>
167+
watchers(true)
168+
.then(() => list(2)));
169+
});
170+
171+
Debugger.on('scriptParsed', (script) => {
172+
const { scriptId, url } = script;
173+
if (url) {
174+
knownScripts[scriptId] = Object.assign({
175+
isNative: url.replace('.js', '') in NATIVES || url === 'bootstrap_node.js',
176+
}, script);
177+
}
178+
});
179+
64180
const replOptions = {
65181
prompt: 'debug> ',
66182
input: inspector.stdin,

lib/node-inspect.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class NodeInspector {
9696
this.client = new ProtocolClient(options.port, options.host);
9797

9898
['Debugger', 'Runtime'].forEach(domain => {
99-
this[domain] = createAgentProxy(domain, this);
99+
this[domain] = createAgentProxy(domain, this.client);
100100
});
101101
this.handleDebugEvent = (fullName, params) => {
102102
const [domain, name] = fullName.split('.');
@@ -124,6 +124,20 @@ class NodeInspector {
124124
this.paused = false;
125125
}
126126

127+
suspendReplWhile(fn) {
128+
this.repl.rli.pause();
129+
this.stdin.pause();
130+
this.paused = true;
131+
return new Promise((resolve) => {
132+
resolve(fn());
133+
}).then(() => {
134+
this.paused = false;
135+
this.repl.rli.resume();
136+
this.repl.displayPrompt();
137+
this.stdin.resume();
138+
}).then(null, throwUnexpectedError);
139+
}
140+
127141
killChild() {
128142
this.client.reset();
129143
if (this.child) {

test/cli/break.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
const tap = require('tap');
3+
4+
const startCLI = require('./start-cli');
5+
6+
tap.test('break on first line', (t) => {
7+
const cli = startCLI(['examples/empty.js']);
8+
9+
function onFatal(error) {
10+
cli.quit();
11+
throw error;
12+
}
13+
14+
return cli.waitFor(/break/)
15+
.then(() => cli.waitForPrompt())
16+
.then(() => {
17+
t.match(cli.output, 'break in examples/empty.js:2',
18+
'pauses in the first line of the script');
19+
t.match(cli.output, '> 2 });',
20+
'shows the source and marks the current line');
21+
})
22+
.then(() => cli.quit())
23+
.then(null, onFatal);
24+
});

0 commit comments

Comments
 (0)