inspector: --inspect-brk for es modules

Reworked rebase of PR #17360 with feedback

PR-URL: https://github.com/nodejs/node/pull/18194
Fixes: https://github.com/nodejs/node/issues/17340
Reviewed-By: Eugene Ostroukhov <eostroukhov@google.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Guy Bedford 2018-01-17 00:35:54 +02:00
parent a65b0b90c9
commit e7ff00d0c5
22 changed files with 202 additions and 22 deletions

View File

@ -44,6 +44,7 @@ class Loader {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'string'); throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'string');
this.base = base; this.base = base;
this.isMain = true;
// methods which translate input code or other information // methods which translate input code or other information
// into es modules // into es modules
@ -132,7 +133,15 @@ class Loader {
loaderInstance = translators.get(format); loaderInstance = translators.get(format);
} }
job = new ModuleJob(this, url, loaderInstance); let inspectBrk = false;
if (this.isMain) {
if (process._breakFirstLine) {
delete process._breakFirstLine;
inspectBrk = true;
}
this.isMain = false;
}
job = new ModuleJob(this, url, loaderInstance, inspectBrk);
this.moduleMap.set(url, job); this.moduleMap.set(url, job);
return job; return job;
} }

View File

@ -14,7 +14,7 @@ const enableDebug = (process.env.NODE_DEBUG || '').match(/\besm\b/) ||
class ModuleJob { class ModuleJob {
// `loader` is the Loader instance used for loading dependencies. // `loader` is the Loader instance used for loading dependencies.
// `moduleProvider` is a function // `moduleProvider` is a function
constructor(loader, url, moduleProvider) { constructor(loader, url, moduleProvider, inspectBrk) {
this.loader = loader; this.loader = loader;
this.error = null; this.error = null;
this.hadError = false; this.hadError = false;
@ -30,6 +30,10 @@ class ModuleJob {
const dependencyJobs = []; const dependencyJobs = [];
({ module: this.module, ({ module: this.module,
reflect: this.reflect } = await this.modulePromise); reflect: this.reflect } = await this.modulePromise);
if (inspectBrk) {
const initWrapper = process.binding('inspector').callAndPauseOnStart;
initWrapper(this.module.instantiate, this.module);
}
assert(this.module instanceof ModuleWrap); assert(this.module instanceof ModuleWrap);
this.module.link(async (dependencySpecifier) => { this.module.link(async (dependencySpecifier) => {
const dependencyJobPromise = const dependencyJobPromise =

View File

@ -467,6 +467,7 @@ Module._load = function(request, parent, isMain) {
ESMLoader = new Loader(); ESMLoader = new Loader();
const userLoader = process.binding('config').userLoader; const userLoader = process.binding('config').userLoader;
if (userLoader) { if (userLoader) {
ESMLoader.isMain = false;
const hooks = await ESMLoader.import(userLoader); const hooks = await ESMLoader.import(userLoader);
ESMLoader = new Loader(); ESMLoader = new Loader();
ESMLoader.hook(hooks); ESMLoader.hook(hooks);

View File

@ -5,7 +5,9 @@ const fs = require('fs');
const http = require('http'); const http = require('http');
const fixtures = require('../common/fixtures'); const fixtures = require('../common/fixtures');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const url = require('url'); const { URL, parse: parseURL } = require('url');
const { getURLFromFilePath } = require('internal/url');
const path = require('path');
const _MAINSCRIPT = fixtures.path('loop.js'); const _MAINSCRIPT = fixtures.path('loop.js');
const DEBUG = false; const DEBUG = false;
@ -171,8 +173,9 @@ class InspectorSession {
const scriptId = script['scriptId']; const scriptId = script['scriptId'];
const url = script['url']; const url = script['url'];
this._scriptsIdsByUrl.set(scriptId, url); this._scriptsIdsByUrl.set(scriptId, url);
if (url === _MAINSCRIPT) if (getURLFromFilePath(url).toString() === this.scriptURL().toString()) {
this.mainScriptId = scriptId; this.mainScriptId = scriptId;
}
} }
if (this._notificationCallback) { if (this._notificationCallback) {
@ -238,11 +241,13 @@ class InspectorSession {
return notification; return notification;
} }
_isBreakOnLineNotification(message, line, url) { _isBreakOnLineNotification(message, line, expectedScriptPath) {
if ('Debugger.paused' === message['method']) { if ('Debugger.paused' === message['method']) {
const callFrame = message['params']['callFrames'][0]; const callFrame = message['params']['callFrames'][0];
const location = callFrame['location']; const location = callFrame['location'];
assert.strictEqual(url, this._scriptsIdsByUrl.get(location['scriptId'])); const scriptPath = this._scriptsIdsByUrl.get(location['scriptId']);
assert(scriptPath.toString() === expectedScriptPath.toString(),
`${scriptPath} !== ${expectedScriptPath}`);
assert.strictEqual(line, location['lineNumber']); assert.strictEqual(line, location['lineNumber']);
return true; return true;
} }
@ -291,12 +296,26 @@ class InspectorSession {
'Waiting for the debugger to disconnect...'); 'Waiting for the debugger to disconnect...');
await this.disconnect(); await this.disconnect();
} }
scriptPath() {
return this._instance.scriptPath();
}
script() {
return this._instance.script();
}
scriptURL() {
return getURLFromFilePath(this.scriptPath());
}
} }
class NodeInstance { class NodeInstance {
constructor(inspectorFlags = ['--inspect-brk=0'], constructor(inspectorFlags = ['--inspect-brk=0'],
scriptContents = '', scriptContents = '',
scriptFile = _MAINSCRIPT) { scriptFile = _MAINSCRIPT) {
this._scriptPath = scriptFile;
this._script = scriptFile ? null : scriptContents;
this._portCallback = null; this._portCallback = null;
this.portPromise = new Promise((resolve) => this._portCallback = resolve); this.portPromise = new Promise((resolve) => this._portCallback = resolve);
this._process = spawnChildProcess(inspectorFlags, scriptContents, this._process = spawnChildProcess(inspectorFlags, scriptContents,
@ -375,7 +394,7 @@ class NodeInstance {
const port = await this.portPromise; const port = await this.portPromise;
return http.get({ return http.get({
port, port,
path: url.parse(devtoolsUrl).path, path: parseURL(devtoolsUrl).path,
headers: { headers: {
'Connection': 'Upgrade', 'Connection': 'Upgrade',
'Upgrade': 'websocket', 'Upgrade': 'websocket',
@ -425,10 +444,16 @@ class NodeInstance {
kill() { kill() {
this._process.kill(); this._process.kill();
} }
}
function readMainScriptSource() { scriptPath() {
return fs.readFileSync(_MAINSCRIPT, 'utf8'); return this._scriptPath;
}
script() {
if (this._script === null)
this._script = fs.readFileSync(this.scriptPath(), 'utf8');
return this._script;
}
} }
function onResolvedOrRejected(promise, callback) { function onResolvedOrRejected(promise, callback) {
@ -469,7 +494,5 @@ function fires(promise, error, timeoutMs) {
} }
module.exports = { module.exports = {
mainScriptPath: _MAINSCRIPT,
readMainScriptSource,
NodeInstance NodeInstance
}; };

10
test/fixtures/es-modules/loop.mjs vendored Normal file
View File

@ -0,0 +1,10 @@
var t = 1;
var k = 1;
console.log('A message', 5);
while (t > 0) {
if (t++ === 1000) {
t = 0;
console.log(`Outputed message #${k++}`);
}
}
process.exit(55);

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -0,0 +1,120 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
const assert = require('assert');
const fixtures = require('../common/fixtures');
const { NodeInstance } = require('../common/inspector-helper.js');
function assertNoUrlsWhileConnected(response) {
assert.strictEqual(response.length, 1);
assert.ok(!response[0].hasOwnProperty('devtoolsFrontendUrl'));
assert.ok(!response[0].hasOwnProperty('webSocketDebuggerUrl'));
}
function assertScopeValues({ result }, expected) {
const unmatched = new Set(Object.keys(expected));
for (const actual of result) {
const value = expected[actual['name']];
assert.strictEqual(actual['value']['value'], value);
unmatched.delete(actual['name']);
}
assert.deepStrictEqual(Array.from(unmatched.values()), []);
}
async function testBreakpointOnStart(session) {
console.log('[test]',
'Verifying debugger stops on start (--inspect-brk option)');
const commands = [
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
{ 'method': 'Debugger.setPauseOnExceptions',
'params': { 'state': 'none' } },
{ 'method': 'Debugger.setAsyncCallStackDepth',
'params': { 'maxDepth': 0 } },
{ 'method': 'Profiler.enable' },
{ 'method': 'Profiler.setSamplingInterval',
'params': { 'interval': 100 } },
{ 'method': 'Debugger.setBlackboxPatterns',
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' }
];
await session.send(commands);
await session.waitForBreakOnLine(0, session.scriptURL());
}
async function testBreakpoint(session) {
console.log('[test]', 'Setting a breakpoint and verifying it is hit');
const commands = [
{ 'method': 'Debugger.setBreakpointByUrl',
'params': { 'lineNumber': 5,
'url': session.scriptURL(),
'columnNumber': 0,
'condition': ''
}
},
{ 'method': 'Debugger.resume' },
];
await session.send(commands);
const { scriptSource } = await session.send({
'method': 'Debugger.getScriptSource',
'params': { 'scriptId': session.mainScriptId } });
assert(scriptSource && (scriptSource.includes(session.script())),
`Script source is wrong: ${scriptSource}`);
await session.waitForConsoleOutput('log', ['A message', 5]);
const paused = await session.waitForBreakOnLine(5, session.scriptURL());
const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId;
console.log('[test]', 'Verify we can read current application state');
const response = await session.send({
'method': 'Runtime.getProperties',
'params': {
'objectId': scopeId,
'ownProperties': false,
'accessorPropertiesOnly': false,
'generatePreview': true
}
});
assertScopeValues(response, { t: 1001, k: 1 });
let { result } = await session.send({
'method': 'Debugger.evaluateOnCallFrame', 'params': {
'callFrameId': '{"ordinal":0,"injectedScriptId":1}',
'expression': 'k + t',
'objectGroup': 'console',
'includeCommandLineAPI': true,
'silent': false,
'returnByValue': false,
'generatePreview': true
}
});
assert.strictEqual(result['value'], 1002);
result = (await session.send({
'method': 'Runtime.evaluate', 'params': {
'expression': '5 * 5'
}
})).result;
assert.strictEqual(result['value'], 25);
}
async function runTest() {
const child = new NodeInstance(['--inspect-brk=0', '--experimental-modules'],
'', fixtures.path('es-modules/loop.mjs'));
const session = await child.connectInspectorSession();
assertNoUrlsWhileConnected(await child.httpGet(null, '/json/list'));
await testBreakpointOnStart(session);
await testBreakpoint(session);
await session.runToCompletion();
assert.strictEqual((await child.expectShutdown()).exitCode, 55);
}
common.crashOnUnhandledRejection();
runTest();

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -1,11 +1,11 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();
const assert = require('assert'); const assert = require('assert');
const { mainScriptPath, const { NodeInstance } = require('../common/inspector-helper.js');
NodeInstance } = require('../common/inspector-helper.js');
async function testBreakpointOnStart(session) { async function testBreakpointOnStart(session) {
const commands = [ const commands = [
@ -24,7 +24,7 @@ async function testBreakpointOnStart(session) {
]; ];
session.send(commands); session.send(commands);
await session.waitForBreakOnLine(0, mainScriptPath); await session.waitForBreakOnLine(0, session.scriptPath());
} }
async function runTests() { async function runTests() {

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
const fixtures = require('../common/fixtures'); const fixtures = require('../common/fixtures');

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -1,3 +1,4 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();

View File

@ -1,12 +1,11 @@
// Flags: --expose-internals
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();
const assert = require('assert'); const assert = require('assert');
const { mainScriptPath, const { NodeInstance } = require('../common/inspector-helper.js');
readMainScriptSource,
NodeInstance } = require('../common/inspector-helper.js');
function checkListResponse(response) { function checkListResponse(response) {
assert.strictEqual(1, response.length); assert.strictEqual(1, response.length);
@ -75,7 +74,7 @@ async function testBreakpointOnStart(session) {
]; ];
await session.send(commands); await session.send(commands);
await session.waitForBreakOnLine(0, mainScriptPath); await session.waitForBreakOnLine(0, session.scriptPath());
} }
async function testBreakpoint(session) { async function testBreakpoint(session) {
@ -83,7 +82,7 @@ async function testBreakpoint(session) {
const commands = [ const commands = [
{ 'method': 'Debugger.setBreakpointByUrl', { 'method': 'Debugger.setBreakpointByUrl',
'params': { 'lineNumber': 5, 'params': { 'lineNumber': 5,
'url': mainScriptPath, 'url': session.scriptPath(),
'columnNumber': 0, 'columnNumber': 0,
'condition': '' 'condition': ''
} }
@ -94,11 +93,11 @@ async function testBreakpoint(session) {
const { scriptSource } = await session.send({ const { scriptSource } = await session.send({
'method': 'Debugger.getScriptSource', 'method': 'Debugger.getScriptSource',
'params': { 'scriptId': session.mainScriptId } }); 'params': { 'scriptId': session.mainScriptId } });
assert(scriptSource && (scriptSource.includes(readMainScriptSource())), assert(scriptSource && (scriptSource.includes(session.script())),
`Script source is wrong: ${scriptSource}`); `Script source is wrong: ${scriptSource}`);
await session.waitForConsoleOutput('log', ['A message', 5]); await session.waitForConsoleOutput('log', ['A message', 5]);
const paused = await session.waitForBreakOnLine(5, mainScriptPath); const paused = await session.waitForBreakOnLine(5, session.scriptPath());
const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId; const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId;
console.log('[test]', 'Verify we can read current application state'); console.log('[test]', 'Verify we can read current application state');