Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

inspector: --inspect-brk for ES6 modules #17360

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion lib/internal/loader/Loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ class Loader {
// an object with the same keys as `exports`, whose values are get/set
// functions for the actual exported values.
this.dynamicInstantiate = undefined;
// Signals that Inspector should break when the first module is loaded
this.inspectorBreak = false;
}

setInspectorBreak() {
this.inspectorBreak = true;
}

hook({ resolve = ModuleRequest.resolve, dynamicInstantiate }) {
Expand Down Expand Up @@ -129,7 +135,13 @@ class Loader {
} else {
loaderInstance = ModuleRequest.loaders.get(format);
}
job = new ModuleJob(this, url, loaderInstance);
let wrapper = null;
if (this.inspectorBreak) {
this.inspectorBreak = false;
wrapper = process.binding('inspector').callAndPauseOnStart;
}

job = new ModuleJob(this, url, loaderInstance, wrapper);
this.moduleMap.set(url, job);
}
return job;
Expand Down
11 changes: 9 additions & 2 deletions lib/internal/loader/ModuleJob.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ const enableDebug = (process.env.NODE_DEBUG || '').match(/\besm\b/) ||
class ModuleJob {
// `loader` is the Loader instance used for loading dependencies.
// `moduleProvider` is a function
constructor(loader, url, moduleProvider) {
// `initWrapper` is a function that wraps module initialization
constructor(loader, url, moduleProvider, initWrapper) {
this.loader = loader;
this.error = null;
this.hadError = false;
this.initWrapper = initWrapper;

// This is a Promise<{ module, reflect }>, whose fields will be copied
// onto `this` by `link()` below once it has been resolved.
Expand Down Expand Up @@ -81,7 +83,12 @@ class ModuleJob {
}
throw e;
}
this.module.instantiate();
if (this.initWrapper) {
this.initWrapper(this.module.instantiate, this.module);
this.initWrapper = null;
} else {
this.module.instantiate();
}
for (const dependencyJob of jobsInGraph) {
// Calling `this.module.instantiate()` instantiates not only the
// ModuleWrap in this module, but all modules in the graph.
Expand Down
4 changes: 4 additions & 0 deletions lib/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,10 @@ Module._load = function(request, parent, isMain) {
ESMLoader = new Loader();
ESMLoader.hook(hooks);
}
if (process._breakFirstLine) {
delete process._breakFirstLine;
ESMLoader.setInspectorBreak();
}
}
Loader.registerImportDynamicallyCallback(ESMLoader);
await ESMLoader.import(getURLFromFilePath(request).pathname);
Expand Down
55 changes: 43 additions & 12 deletions test/common/inspector-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@ const fs = require('fs');
const http = require('http');
const fixtures = require('../common/fixtures');
const { spawn } = require('child_process');
const url = require('url');
const { URL, parse: parseURL } = require('url');

const _MAINSCRIPT = fixtures.path('loop.js');
const DEBUG = false;
const TIMEOUT = common.platformTimeout(15 * 1000);

function normalizeToFileUrl(path) {
const url = new URL(path, 'file:');
if (url.protocol === 'file:')
return url.toString();
// Windows workaround - URL consider 'drive:' (e.g. 'c:'') to be a protocol
return new URL(`/${path}`, 'file:').toString();
}

function sameScriptPath(script1, script2) {
return normalizeToFileUrl(script1) === normalizeToFileUrl(script2);
}

function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) {
const args = [].concat(inspectorFlags);
if (scriptContents) {
Expand Down Expand Up @@ -169,9 +181,8 @@ class InspectorSession {
if (message.method === 'Debugger.scriptParsed') {
const script = message['params'];
const scriptId = script['scriptId'];
const url = script['url'];
this._scriptsIdsByUrl.set(scriptId, url);
if (url === _MAINSCRIPT)
this._scriptsIdsByUrl.set(scriptId, script['url']);
if (sameScriptPath(script['url'], this.scriptURL()))
this.mainScriptId = scriptId;
}

Expand Down Expand Up @@ -238,11 +249,13 @@ class InspectorSession {
return notification;
}

_isBreakOnLineNotification(message, line, url) {
_isBreakOnLineNotification(message, line, expectedScriptPath) {
if ('Debugger.paused' === message['method']) {
const callFrame = message['params']['callFrames'][0];
const location = callFrame['location'];
assert.strictEqual(url, this._scriptsIdsByUrl.get(location['scriptId']));
const scriptPath = this._scriptsIdsByUrl.get(location['scriptId']);
assert(sameScriptPath(scriptPath, expectedScriptPath),
`${scriptPath} !== ${expectedScriptPath}`);
assert.strictEqual(line, location['lineNumber']);
return true;
}
Expand Down Expand Up @@ -291,12 +304,26 @@ class InspectorSession {
'Waiting for the debugger to disconnect...');
await this.disconnect();
}

scriptPath() {
return this._instance.scriptPath();
}

script() {
return this._instance.script();
}

scriptURL() {
return new URL(this.scriptPath(), 'file:').toString();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

}
}

class NodeInstance {
constructor(inspectorFlags = ['--inspect-brk=0'],
scriptContents = '',
scriptFile = _MAINSCRIPT) {
this._scriptPath = scriptFile;
this._script = scriptFile ? null : scriptContents;
this._portCallback = null;
this.portPromise = new Promise((resolve) => this._portCallback = resolve);
this._process = spawnChildProcess(inspectorFlags, scriptContents,
Expand Down Expand Up @@ -373,7 +400,7 @@ class NodeInstance {
return this.portPromise.then((port) => new Promise((resolve) => {
http.get({
port,
path: url.parse(devtoolsUrl).path,
path: parseURL(devtoolsUrl).path,
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
Expand Down Expand Up @@ -406,10 +433,16 @@ class NodeInstance {
kill() {
this._process.kill();
}
}

function readMainScriptSource() {
return fs.readFileSync(_MAINSCRIPT, 'utf8');
scriptPath() {
return this._scriptPath;
}

script() {
if (this._script === null)
this._script = fs.readFileSync(this.scriptPath(), 'utf8');
return this._script;
}
}

function onResolvedOrRejected(promise, callback) {
Expand Down Expand Up @@ -450,7 +483,5 @@ function fires(promise, error, timeoutMs) {
}

module.exports = {
mainScriptPath: _MAINSCRIPT,
readMainScriptSource,
NodeInstance
};
10 changes: 10 additions & 0 deletions test/fixtures/loop.mjs
Original file line number Diff line number Diff line change
@@ -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);
119 changes: 119 additions & 0 deletions test/parallel/test-inspector-esm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'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('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();
5 changes: 2 additions & 3 deletions test/sequential/test-inspector-debug-brk-flag.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ const common = require('../common');
common.skipIfInspectorDisabled();

const assert = require('assert');
const { mainScriptPath,
NodeInstance } = require('../common/inspector-helper.js');
const { NodeInstance } = require('../common/inspector-helper.js');

async function testBreakpointOnStart(session) {
const commands = [
Expand All @@ -24,7 +23,7 @@ async function testBreakpointOnStart(session) {
];

session.send(commands);
await session.waitForBreakOnLine(0, mainScriptPath);
await session.waitForBreakOnLine(0, session.scriptPath());
}

async function runTests() {
Expand Down
12 changes: 5 additions & 7 deletions test/sequential/test-inspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ const common = require('../common');
common.skipIfInspectorDisabled();

const assert = require('assert');
const { mainScriptPath,
readMainScriptSource,
NodeInstance } = require('../common/inspector-helper.js');
const { NodeInstance } = require('../common/inspector-helper.js');

function checkListResponse(response) {
assert.strictEqual(1, response.length);
Expand Down Expand Up @@ -75,15 +73,15 @@ async function testBreakpointOnStart(session) {
];

await session.send(commands);
await session.waitForBreakOnLine(0, mainScriptPath);
await session.waitForBreakOnLine(0, session.scriptPath());
}

async function testBreakpoint(session) {
console.log('[test]', 'Setting a breakpoint and verifying it is hit');
const commands = [
{ 'method': 'Debugger.setBreakpointByUrl',
'params': { 'lineNumber': 5,
'url': mainScriptPath,
'url': session.scriptPath(),
'columnNumber': 0,
'condition': ''
}
Expand All @@ -94,11 +92,11 @@ async function testBreakpoint(session) {
const { scriptSource } = await session.send({
'method': 'Debugger.getScriptSource',
'params': { 'scriptId': session.mainScriptId } });
assert(scriptSource && (scriptSource.includes(readMainScriptSource())),
assert(scriptSource && (scriptSource.includes(session.script())),
`Script source is wrong: ${scriptSource}`);

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;

console.log('[test]', 'Verify we can read current application state');
Expand Down