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

Commit df6b89d

Browse files
author
Jan Krems
committed
feat: Paused & global exec
1 parent 0d31560 commit df6b89d

File tree

5 files changed

+187
-54
lines changed

5 files changed

+187
-54
lines changed

examples/alive.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
let x = 0;
3+
function heartbeat() {
4+
++x;
5+
}
6+
setInterval(heartbeat, 50);

lib/internal/inspect-protocol.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ class Client extends EventEmitter {
171171
}
172172

173173
_handleChunk(chunk) {
174-
debuglog('data on websocket');
175174
this._unprocessed = Buffer.concat([this._unprocessed, chunk]);
176175

177176
while (this._unprocessed.length > 2) {

lib/internal/inspect-repl.js

Lines changed: 147 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const Repl = require('repl');
2525
const util = require('util');
2626
const vm = require('vm');
2727

28+
const debuglog = util.debuglog('inspect');
29+
2830
const NATIVES = process.binding('natives');
2931

3032
const SHORTCUTS = {
@@ -211,8 +213,15 @@ function createRepl(inspector) {
211213
return util.inspect(value, INSPECT_OPTIONS);
212214
}
213215

216+
function getCurrentLocation() {
217+
if (!selectedFrame) {
218+
throw new Error('Requires execution to be paused');
219+
}
220+
return selectedFrame.location;
221+
}
222+
214223
function isCurrentScript(script) {
215-
return selectedFrame && selectedFrame.location.scriptId === script.scriptId;
224+
return getCurrentLocation().scriptId === script.scriptId;
216225
}
217226

218227
function formatScripts(displayNatives = false) {
@@ -274,17 +283,23 @@ function createRepl(inspector) {
274283
}
275284

276285
function controlEval(input, context, filename, callback) {
286+
debuglog('eval:', input);
287+
function returnToCallback(error, result) {
288+
debuglog('end-eval:', input, error);
289+
callback(error, result);
290+
}
291+
277292
try {
278293
const code = prepareControlCode(input);
279294
const result = vm.runInContext(code, context, filename);
280295

281296
if (result && typeof result.then === 'function') {
282-
toCallback(result, callback);
297+
toCallback(result, returnToCallback);
283298
return;
284299
}
285-
callback(null, result);
300+
returnToCallback(null, result);
286301
} catch (e) {
287-
callback(e);
302+
returnToCallback(e);
288303
}
289304
}
290305

@@ -294,20 +309,24 @@ function createRepl(inspector) {
294309
return Promise.reject('client.reqScopes not implemented');
295310
}
296311

297-
const params = {
298-
callFrameId: selectedFrame.callFrameId,
312+
if (selectedFrame) {
313+
return Debugger.evaluateOnCallFrame({
314+
callFrameId: selectedFrame.callFrameId,
315+
expression: code,
316+
objectGroup: 'node-inspect',
317+
generatePreview: true,
318+
}).then(convertResultToRemoteObject);
319+
}
320+
return Runtime.evaluate({
299321
expression: code,
300322
objectGroup: 'node-inspect',
301323
generatePreview: true,
302-
};
303-
304-
return Debugger.evaluateOnCallFrame(params)
305-
.then(convertResultToRemoteObject);
324+
}).then(convertResultToRemoteObject);
306325
}
307326

308-
function watchers(verbose = false) {
327+
function formatWatchers(verbose = false) {
309328
if (!watchedExpressions.length) {
310-
return Promise.resolve();
329+
return Promise.resolve('');
311330
}
312331

313332
const inspectValue = expr =>
@@ -317,38 +336,39 @@ function createRepl(inspector) {
317336

318337
return Promise.all(watchedExpressions.map(inspectValue))
319338
.then(values => {
320-
if (verbose) print('Watchers:');
321-
322-
watchedExpressions.forEach((expr, idx) => {
323-
const prefix = `${leftPad(idx, ' ', watchedExpressions.length - 1)}: ${expr} =`;
324-
const value = inspect(values[idx], { colors: true });
325-
if (value.indexOf('\n') === -1) {
326-
print(`${prefix} ${value}`);
327-
} else {
328-
print(`${prefix}\n ${value.split('\n').join('\n ')}`);
329-
}
330-
});
339+
const lines = watchedExpressions
340+
.map((expr, idx) => {
341+
const prefix = `${leftPad(idx, ' ', watchedExpressions.length - 1)}: ${expr} =`;
342+
const value = inspect(values[idx], { colors: true });
343+
if (value.indexOf('\n') === -1) {
344+
return `${prefix} ${value}`;
345+
}
346+
return `${prefix}\n ${value.split('\n').join('\n ')}`;
347+
});
348+
return lines.join('\n');
349+
})
350+
.then((valueList) => (verbose ? `Watchers:\n${valueList}\n` : valueList));
351+
}
331352

332-
if (verbose) print('');
333-
});
353+
function watchers(verbose = false) {
354+
return formatWatchers(verbose).then(print);
334355
}
335356

336-
// List source code
337-
function list(delta = 5) {
338-
const { scriptId, lineNumber, columnNumber } = selectedFrame.location;
357+
function formatSourceContext(delta = 5) {
358+
const { scriptId, lineNumber, columnNumber } = getCurrentLocation();
339359
const start = Math.max(1, lineNumber - delta + 1);
340360
const end = lineNumber + delta + 1;
341361

342362
return Debugger.getScriptSource({ scriptId })
343363
.then(({ scriptSource }) => {
344364
const lines = scriptSource.split('\n');
345-
for (let i = start; i <= lines.length && i <= end; ++i) {
365+
return lines.slice(start - 1, end).map((lineText, offset) => {
366+
const i = start + offset;
346367
const isCurrent = i === (lineNumber + 1);
347368

348-
let lineText = lines[i - 1];
349-
if (isCurrent) {
350-
lineText = markSourceColumn(lineText, columnNumber, inspector.repl);
351-
}
369+
const markedLine = isCurrent
370+
? markSourceColumn(lineText, columnNumber, inspector.repl)
371+
: lineText;
352372

353373
let isBreakpoint = false;
354374
knownBreakpoints.forEach(({ location }) => {
@@ -363,10 +383,15 @@ function createRepl(inspector) {
363383
} else if (isBreakpoint) {
364384
prefixChar = '*';
365385
}
366-
print(`${leftPad(i, prefixChar, end)} ${lineText}`);
367-
}
368-
})
369-
.then(null, error => {
386+
return `${leftPad(i, prefixChar, end)} ${markedLine}`;
387+
}).join('\n');
388+
});
389+
}
390+
391+
// List source code
392+
function list(delta = 5) {
393+
return formatSourceContext(delta)
394+
.then(print, (error) => {
370395
print('You can\'t list source code right now');
371396
throw error;
372397
});
@@ -392,9 +417,10 @@ function createRepl(inspector) {
392417
const scriptUrl = script ? script.url : location.scriptUrl;
393418
return `${getRelativePath(scriptUrl)}:${location.lineNumber + 1}`;
394419
}
395-
knownBreakpoints.forEach((bp, idx) => {
396-
print(`#${idx} ${formatLocation(bp.location)}`);
397-
});
420+
const breaklist = knownBreakpoints
421+
.map((bp, idx) => `#${idx} ${formatLocation(bp.location)}`)
422+
.join('\n');
423+
print(breaklist);
398424
}
399425

400426
function setBreakpoint(script, line, condition, silent) {
@@ -410,17 +436,16 @@ function createRepl(inspector) {
410436

411437
// setBreakpoint(): set breakpoint at current location
412438
if (script === undefined) {
413-
// TODO: assertIsPaused()
414-
return Debugger.setBreakpoint({ location: selectedFrame.location, condition })
439+
return Debugger.setBreakpoint({ location: getCurrentLocation(), condition })
415440
.then(registerBreakpoint);
416441
}
417442

418443
// setBreakpoint(line): set breakpoint in current script at specific line
419444
if (line === undefined && typeof script === 'number') {
420-
// TODO: assertIsPaused()
421-
const location = Object.assign({}, selectedFrame.location, {
445+
const location = {
446+
scriptId: getCurrentLocation().scriptId,
422447
lineNumber: script - 1,
423-
});
448+
};
424449
return Debugger.setBreakpoint({ location, condition })
425450
.then(registerBreakpoint);
426451
}
@@ -431,12 +456,18 @@ function createRepl(inspector) {
431456

432457
// setBreakpoint('fn()'): Break when a function is called
433458
if (script.endsWith('()')) {
434-
// TODO: handle !currentFrame (~Runtime.evaluate)
435-
return Debugger.evaluateOnCallFrame({
436-
callFrameId: selectedFrame.callFrameId,
437-
expression: `debug(${script.slice(0, -2)})`,
438-
includeCommandLineAPI: true,
439-
}).then(({ result, wasThrown }) => {
459+
const debugExpr = `debug(${script.slice(0, -2)})`;
460+
const debugCall = selectedFrame
461+
? Debugger.evaluateOnCallFrame({
462+
callFrameId: selectedFrame.callFrameId,
463+
expression: debugExpr,
464+
includeCommandLineAPI: true,
465+
})
466+
: Runtime.evaluate({
467+
expression: debugExpr,
468+
includeCommandLineAPI: true,
469+
});
470+
return debugCall.then(({ result, wasThrown }) => {
440471
if (wasThrown) return convertResultToError(result);
441472
return undefined; // This breakpoint can't be removed the same way
442473
});
@@ -517,10 +548,18 @@ function createRepl(inspector) {
517548
print(`${reason === 'other' ? 'break' : reason} in ${scriptUrl}:${lineNumber + 1}`);
518549

519550
inspector.suspendReplWhile(() =>
520-
watchers(true)
521-
.then(() => list(2)));
551+
Promise.all([formatWatchers(true), formatSourceContext(2)])
552+
.then(([watcherList, context]) => (watcherList ? `${watcherList}\n${context}` : context))
553+
.then(print));
522554
});
523555

556+
function handleResumed() {
557+
currentBacktrace = null;
558+
selectedFrame = null;
559+
}
560+
561+
Debugger.on('resumed', handleResumed);
562+
524563
Debugger.on('breakpointResolved', handleBreakpointResolved);
525564

526565
Debugger.on('scriptParsed', (script) => {
@@ -535,18 +574,22 @@ function createRepl(inspector) {
535574
function initializeContext(context) {
536575
copyOwnProperties(context, {
537576
get cont() {
577+
handleResumed();
538578
return Debugger.resume();
539579
},
540580

541581
get next() {
582+
handleResumed();
542583
return Debugger.stepOver();
543584
},
544585

545586
get step() {
587+
handleResumed();
546588
return Debugger.stepInto();
547589
},
548590

549591
get out() {
592+
handleResumed();
550593
return Debugger.stepOut();
551594
},
552595

@@ -583,6 +626,57 @@ function createRepl(inspector) {
583626
watchedExpressions.splice(index !== -1 ? index : +expr, 1);
584627
},
585628

629+
get repl() {
630+
// if (!this.requireConnection()) return;
631+
print('Press Ctrl + C to leave debug repl');
632+
633+
// Don't display any default messages
634+
const listeners = repl.rli.listeners('SIGINT').slice(0);
635+
repl.rli.removeAllListeners('SIGINT');
636+
637+
function exitDebugRepl() {
638+
// Restore all listeners
639+
process.nextTick(() => {
640+
listeners.forEach((listener) => {
641+
repl.rli.on('SIGINT', listener);
642+
});
643+
});
644+
645+
// Exit debug repl
646+
repl.eval = controlEval;
647+
648+
// Swap history
649+
history.debug = repl.rli.history;
650+
repl.rli.history = history.control;
651+
652+
repl.context = inspector.context;
653+
repl.rli.setPrompt('debug> ');
654+
repl.displayPrompt();
655+
656+
repl.rli.removeListener('SIGINT', exitDebugRepl);
657+
repl.removeListener('exit', exitDebugRepl);
658+
}
659+
660+
// Exit debug repl on SIGINT
661+
repl.rli.on('SIGINT', exitDebugRepl);
662+
663+
// Exit debug repl on repl exit
664+
repl.on('exit', exitDebugRepl);
665+
666+
// Set new
667+
repl.eval = (code, evalCtx, file, cb) => {
668+
toCallback(debugEval(code), cb);
669+
};
670+
repl.context = {};
671+
672+
// Swap history
673+
history.control = repl.rli.history;
674+
repl.rli.history = history.debug;
675+
676+
repl.rli.setPrompt('> ');
677+
repl.displayPrompt();
678+
},
679+
586680
get version() {
587681
return Runtime.evaluate({
588682
expression: 'process.versions.v8',

test/cli/break.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ test('stepping through breakpoints', (t) => {
4747

4848
// Prepare additional breakpoints
4949
.then(() => cli.command('sb("break.js", 6)'))
50+
.then(() => t.notMatch(cli.output, 'Could not resolve breakpoint'))
5051
.then(() => cli.command('sb("otherFunction()")'))
5152
.then(() => cli.command('sb(16)'))
53+
.then(() => t.notMatch(cli.output, 'Could not resolve breakpoint'))
5254
.then(() => cli.command('breakpoints'))
5355
.then(() => {
5456
t.match(cli.output, '#0 examples/break.js:6');

test/cli/exec.test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict';
2+
const { test } = require('tap');
3+
4+
const startCLI = require('./start-cli');
5+
6+
test('examples/alive.js', (t) => {
7+
const cli = startCLI(['examples/alive.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(() => cli.command('exec [typeof heartbeat, typeof process.exit]'))
17+
.then(() => {
18+
t.match(cli.output, '[ \'function\', \'function\' ]', 'works w/o paren');
19+
})
20+
.then(() => cli.command('exec("[typeof heartbeat, typeof process.exit]")'))
21+
.then(() => {
22+
t.match(cli.output, '[ \'function\', \'function\' ]', 'works w/ paren');
23+
})
24+
.then(() => cli.command('cont'))
25+
.then(() => cli.command('exec [typeof heartbeat, typeof process.exit]'))
26+
.then(() => {
27+
t.match(cli.output, '[ \'undefined\', \'function\' ]',
28+
'non-paused exec can see global but not module-scope values');
29+
})
30+
.then(() => cli.quit())
31+
.then(null, onFatal);
32+
});

0 commit comments

Comments
 (0)