mirror of
https://github.com/nodejs/node.git
synced 2024-12-01 16:10:02 +01:00
f01fd2ae70
Currently Inspector posts a V8 "task" when a message is incoming. To make sure messages are processed even when no JS is executed (e.g. while waiting for I/O or timer), inspector will now post a libuv request. Fixes: https://github.com/nodejs/node/issues/11589 PR-URL: https://github.com/nodejs/node/pull/11617 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
476 lines
13 KiB
JavaScript
476 lines
13 KiB
JavaScript
'use strict';
|
|
const common = require('../common');
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const http = require('http');
|
|
const path = require('path');
|
|
const spawn = require('child_process').spawn;
|
|
const url = require('url');
|
|
|
|
const DEBUG = false;
|
|
|
|
const TIMEOUT = 15 * 1000;
|
|
|
|
const mainScript = path.join(common.fixturesDir, 'loop.js');
|
|
|
|
function send(socket, message, id, callback) {
|
|
const msg = JSON.parse(JSON.stringify(message)); // Clone!
|
|
msg['id'] = id;
|
|
if (DEBUG)
|
|
console.log('[sent]', JSON.stringify(msg));
|
|
const messageBuf = Buffer.from(JSON.stringify(msg));
|
|
|
|
const wsHeaderBuf = Buffer.allocUnsafe(16);
|
|
wsHeaderBuf.writeUInt8(0x81, 0);
|
|
let byte2 = 0x80;
|
|
const bodyLen = messageBuf.length;
|
|
let maskOffset = 2;
|
|
if (bodyLen < 126) {
|
|
byte2 = 0x80 + bodyLen;
|
|
} else if (bodyLen < 65536) {
|
|
byte2 = 0xFE;
|
|
wsHeaderBuf.writeUInt16BE(bodyLen, 2);
|
|
maskOffset = 4;
|
|
} else {
|
|
byte2 = 0xFF;
|
|
wsHeaderBuf.writeUInt32BE(bodyLen, 2);
|
|
wsHeaderBuf.writeUInt32BE(0, 6);
|
|
maskOffset = 10;
|
|
}
|
|
wsHeaderBuf.writeUInt8(byte2, 1);
|
|
wsHeaderBuf.writeUInt32BE(0x01020408, maskOffset);
|
|
|
|
for (let i = 0; i < messageBuf.length; i++)
|
|
messageBuf[i] = messageBuf[i] ^ (1 << (i % 4));
|
|
socket.write(
|
|
Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]),
|
|
callback);
|
|
}
|
|
|
|
function parseWSFrame(buffer, handler) {
|
|
if (buffer.length < 2)
|
|
return 0;
|
|
assert.strictEqual(0x81, buffer[0]);
|
|
let dataLen = 0x7F & buffer[1];
|
|
let bodyOffset = 2;
|
|
if (buffer.length < bodyOffset + dataLen)
|
|
return 0;
|
|
if (dataLen === 126) {
|
|
dataLen = buffer.readUInt16BE(2);
|
|
bodyOffset = 4;
|
|
} else if (dataLen === 127) {
|
|
dataLen = buffer.readUInt32BE(2);
|
|
bodyOffset = 10;
|
|
}
|
|
if (buffer.length < bodyOffset + dataLen)
|
|
return 0;
|
|
const message = JSON.parse(
|
|
buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8'));
|
|
if (DEBUG)
|
|
console.log('[received]', JSON.stringify(message));
|
|
handler(message);
|
|
return bodyOffset + dataLen;
|
|
}
|
|
|
|
function tearDown(child, err) {
|
|
child.kill();
|
|
if (err instanceof Error) {
|
|
console.error(err.stack);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function checkHttpResponse(port, path, callback) {
|
|
http.get({port, path}, function(res) {
|
|
let response = '';
|
|
res.setEncoding('utf8');
|
|
res
|
|
.on('data', (data) => response += data.toString())
|
|
.on('end', () => {
|
|
let err = null;
|
|
let json = undefined;
|
|
try {
|
|
json = JSON.parse(response);
|
|
} catch (e) {
|
|
err = e;
|
|
err.response = response;
|
|
}
|
|
callback(err, json);
|
|
});
|
|
});
|
|
}
|
|
|
|
function makeBufferingDataCallback(dataCallback) {
|
|
let buffer = Buffer.alloc(0);
|
|
return (data) => {
|
|
const newData = Buffer.concat([buffer, data]);
|
|
const str = newData.toString('utf8');
|
|
const lines = str.split('\n');
|
|
if (str.endsWith('\n'))
|
|
buffer = Buffer.alloc(0);
|
|
else
|
|
buffer = Buffer.from(lines.pop(), 'utf8');
|
|
for (const line of lines)
|
|
dataCallback(line);
|
|
};
|
|
}
|
|
|
|
function timeout(message, multiplicator) {
|
|
return setTimeout(common.mustNotCall(message),
|
|
TIMEOUT * (multiplicator || 1));
|
|
}
|
|
|
|
const TestSession = function(socket, harness) {
|
|
this.mainScriptPath = harness.mainScriptPath;
|
|
this.mainScriptId = null;
|
|
|
|
this.harness_ = harness;
|
|
this.socket_ = socket;
|
|
this.expectClose_ = false;
|
|
this.scripts_ = {};
|
|
this.messagefilter_ = null;
|
|
this.responseCheckers_ = {};
|
|
this.lastId_ = 0;
|
|
this.messages_ = {};
|
|
this.expectedId_ = 1;
|
|
this.lastMessageResponseCallback_ = null;
|
|
|
|
let buffer = Buffer.alloc(0);
|
|
socket.on('data', (data) => {
|
|
buffer = Buffer.concat([buffer, data]);
|
|
let consumed;
|
|
do {
|
|
consumed = parseWSFrame(buffer, this.processMessage_.bind(this));
|
|
if (consumed)
|
|
buffer = buffer.slice(consumed);
|
|
} while (consumed);
|
|
}).on('close', () => assert(this.expectClose_, 'Socket closed prematurely'));
|
|
};
|
|
|
|
TestSession.prototype.scriptUrlForId = function(id) {
|
|
return this.scripts_[id];
|
|
};
|
|
|
|
TestSession.prototype.processMessage_ = function(message) {
|
|
const method = message['method'];
|
|
if (method === 'Debugger.scriptParsed') {
|
|
const script = message['params'];
|
|
const scriptId = script['scriptId'];
|
|
const url = script['url'];
|
|
this.scripts_[scriptId] = url;
|
|
if (url === mainScript)
|
|
this.mainScriptId = scriptId;
|
|
}
|
|
this.messagefilter_ && this.messagefilter_(message);
|
|
const id = message['id'];
|
|
if (id) {
|
|
assert.strictEqual(id, this.expectedId_);
|
|
this.expectedId_++;
|
|
if (this.responseCheckers_[id]) {
|
|
assert(message['result'], JSON.stringify(message) + ' (response to ' +
|
|
JSON.stringify(this.messages_[id]) + ')');
|
|
this.responseCheckers_[id](message['result']);
|
|
delete this.responseCheckers_[id];
|
|
}
|
|
assert(!message['error'], JSON.stringify(message) + ' (replying to ' +
|
|
JSON.stringify(this.messages_[id]) + ')');
|
|
delete this.messages_[id];
|
|
if (id === this.lastId_) {
|
|
this.lastMessageResponseCallback_ && this.lastMessageResponseCallback_();
|
|
this.lastMessageResponseCallback_ = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
TestSession.prototype.sendAll_ = function(commands, callback) {
|
|
if (!commands.length) {
|
|
callback();
|
|
} else {
|
|
this.lastId_++;
|
|
let command = commands[0];
|
|
if (command instanceof Array) {
|
|
this.responseCheckers_[this.lastId_] = command[1];
|
|
command = command[0];
|
|
}
|
|
if (command instanceof Function)
|
|
command = command();
|
|
this.messages_[this.lastId_] = command;
|
|
send(this.socket_, command, this.lastId_,
|
|
() => this.sendAll_(commands.slice(1), callback));
|
|
}
|
|
};
|
|
|
|
TestSession.prototype.sendInspectorCommands = function(commands) {
|
|
if (!(commands instanceof Array))
|
|
commands = [commands];
|
|
return this.enqueue((callback) => {
|
|
let timeoutId = null;
|
|
this.lastMessageResponseCallback_ = () => {
|
|
timeoutId && clearTimeout(timeoutId);
|
|
callback();
|
|
};
|
|
this.sendAll_(commands, () => {
|
|
timeoutId = setTimeout(() => {
|
|
let s = '';
|
|
for (const id in this.messages_) {
|
|
s += id + ', ';
|
|
}
|
|
common.fail('Messages without response: ' +
|
|
s.substring(0, s.length - 2));
|
|
}, TIMEOUT);
|
|
});
|
|
});
|
|
};
|
|
|
|
TestSession.prototype.createCallbackWithTimeout_ = function(message) {
|
|
const promise = new Promise((resolve) => {
|
|
this.enqueue((callback) => {
|
|
const timeoutId = timeout(message);
|
|
resolve(() => {
|
|
clearTimeout(timeoutId);
|
|
callback();
|
|
});
|
|
});
|
|
});
|
|
return () => promise.then((callback) => callback());
|
|
};
|
|
|
|
TestSession.prototype.expectMessages = function(expects) {
|
|
if (!(expects instanceof Array)) expects = [ expects ];
|
|
|
|
const callback = this.createCallbackWithTimeout_(
|
|
'Matching response was not received:\n' + expects[0]);
|
|
this.messagefilter_ = (message) => {
|
|
if (expects[0](message))
|
|
expects.shift();
|
|
if (!expects.length) {
|
|
this.messagefilter_ = null;
|
|
callback();
|
|
}
|
|
};
|
|
return this;
|
|
};
|
|
|
|
TestSession.prototype.expectStderrOutput = function(regexp) {
|
|
this.harness_.addStderrFilter(
|
|
regexp,
|
|
this.createCallbackWithTimeout_('Timed out waiting for ' + regexp));
|
|
return this;
|
|
};
|
|
|
|
TestSession.prototype.runNext_ = function() {
|
|
if (this.task_) {
|
|
setImmediate(() => {
|
|
this.task_(() => {
|
|
this.task_ = this.task_.next_;
|
|
this.runNext_();
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
TestSession.prototype.enqueue = function(task) {
|
|
if (!this.task_) {
|
|
this.task_ = task;
|
|
this.runNext_();
|
|
} else {
|
|
let t = this.task_;
|
|
while (t.next_)
|
|
t = t.next_;
|
|
t.next_ = task;
|
|
}
|
|
return this;
|
|
};
|
|
|
|
TestSession.prototype.disconnect = function(childDone) {
|
|
return this.enqueue((callback) => {
|
|
this.expectClose_ = true;
|
|
this.harness_.childInstanceDone =
|
|
this.harness_.childInstanceDone || childDone;
|
|
this.socket_.destroy();
|
|
console.log('[test]', 'Connection terminated');
|
|
callback();
|
|
});
|
|
};
|
|
|
|
TestSession.prototype.testHttpResponse = function(path, check) {
|
|
return this.enqueue((callback) =>
|
|
checkHttpResponse(this.harness_.port, path, (err, response) => {
|
|
check.call(this, err, response);
|
|
callback();
|
|
}));
|
|
};
|
|
|
|
|
|
const Harness = function(port, childProcess) {
|
|
this.port = port;
|
|
this.mainScriptPath = mainScript;
|
|
this.stderrFilters_ = [];
|
|
this.process_ = childProcess;
|
|
this.childInstanceDone = false;
|
|
this.returnCode_ = null;
|
|
this.running_ = true;
|
|
|
|
childProcess.stdout.on('data', makeBufferingDataCallback(
|
|
(line) => console.log('[out]', line)));
|
|
|
|
|
|
childProcess.stderr.on('data', makeBufferingDataCallback((message) => {
|
|
const pending = [];
|
|
console.log('[err]', message);
|
|
for (const filter of this.stderrFilters_)
|
|
if (!filter(message)) pending.push(filter);
|
|
this.stderrFilters_ = pending;
|
|
}));
|
|
childProcess.on('exit', (code, signal) => {
|
|
assert(this.childInstanceDone, 'Child instance died prematurely');
|
|
this.returnCode_ = code;
|
|
this.running_ = false;
|
|
});
|
|
};
|
|
|
|
Harness.prototype.addStderrFilter = function(regexp, callback) {
|
|
this.stderrFilters_.push((message) => {
|
|
if (message.match(regexp)) {
|
|
callback();
|
|
return true;
|
|
}
|
|
});
|
|
};
|
|
|
|
Harness.prototype.run_ = function() {
|
|
setImmediate(() => {
|
|
this.task_(() => {
|
|
this.task_ = this.task_.next_;
|
|
if (this.task_)
|
|
this.run_();
|
|
});
|
|
});
|
|
};
|
|
|
|
Harness.prototype.enqueue_ = function(task) {
|
|
if (!this.task_) {
|
|
this.task_ = task;
|
|
this.run_();
|
|
} else {
|
|
let chain = this.task_;
|
|
while (chain.next_)
|
|
chain = chain.next_;
|
|
chain.next_ = task;
|
|
}
|
|
return this;
|
|
};
|
|
|
|
Harness.prototype.testHttpResponse = function(path, check) {
|
|
return this.enqueue_((doneCallback) => {
|
|
checkHttpResponse(this.port, path, (err, response) => {
|
|
check.call(this, err, response);
|
|
doneCallback();
|
|
});
|
|
});
|
|
};
|
|
|
|
Harness.prototype.wsHandshake = function(devtoolsUrl, tests, readyCallback) {
|
|
http.get({
|
|
port: this.port,
|
|
path: url.parse(devtoolsUrl).path,
|
|
headers: {
|
|
'Connection': 'Upgrade',
|
|
'Upgrade': 'websocket',
|
|
'Sec-WebSocket-Version': 13,
|
|
'Sec-WebSocket-Key': 'key=='
|
|
}
|
|
}).on('upgrade', (message, socket) => {
|
|
const session = new TestSession(socket, this);
|
|
if (!(tests instanceof Array))
|
|
tests = [tests];
|
|
function enqueue(tests) {
|
|
session.enqueue((sessionCb) => {
|
|
if (tests.length) {
|
|
tests[0](session);
|
|
session.enqueue((cb2) => {
|
|
enqueue(tests.slice(1));
|
|
cb2();
|
|
});
|
|
} else {
|
|
readyCallback();
|
|
}
|
|
sessionCb();
|
|
});
|
|
}
|
|
enqueue(tests);
|
|
}).on('response', common.mustNotCall('Upgrade was not received'));
|
|
};
|
|
|
|
Harness.prototype.runFrontendSession = function(tests) {
|
|
return this.enqueue_((callback) => {
|
|
checkHttpResponse(this.port, '/json/list', (err, response) => {
|
|
assert.ifError(err);
|
|
this.wsHandshake(response[0]['webSocketDebuggerUrl'], tests, callback);
|
|
});
|
|
});
|
|
};
|
|
|
|
Harness.prototype.expectShutDown = function(errorCode) {
|
|
this.enqueue_((callback) => {
|
|
if (this.running_) {
|
|
const timeoutId = timeout('Have not terminated');
|
|
this.process_.on('exit', (code) => {
|
|
clearTimeout(timeoutId);
|
|
assert.strictEqual(errorCode, code);
|
|
callback();
|
|
});
|
|
} else {
|
|
assert.strictEqual(errorCode, this.returnCode_);
|
|
callback();
|
|
}
|
|
});
|
|
};
|
|
|
|
Harness.prototype.kill = function() {
|
|
return this.enqueue_((callback) => {
|
|
this.process_.kill();
|
|
callback();
|
|
});
|
|
};
|
|
|
|
exports.startNodeForInspectorTest = function(callback,
|
|
inspectorFlag = '--inspect-brk',
|
|
opt_script_contents) {
|
|
const args = [inspectorFlag];
|
|
if (opt_script_contents) {
|
|
args.push('-e', opt_script_contents);
|
|
} else {
|
|
args.push(mainScript);
|
|
}
|
|
|
|
const child = spawn(process.execPath, args);
|
|
|
|
const timeoutId = timeout('Child process did not start properly', 4);
|
|
|
|
let found = false;
|
|
|
|
const dataCallback = makeBufferingDataCallback((text) => {
|
|
clearTimeout(timeoutId);
|
|
console.log('[err]', text);
|
|
if (found) return;
|
|
const match = text.match(/Debugger listening on port (\d+)/);
|
|
found = true;
|
|
child.stderr.removeListener('data', dataCallback);
|
|
assert.ok(match, text);
|
|
callback(new Harness(match[1], child));
|
|
});
|
|
|
|
child.stderr.on('data', dataCallback);
|
|
|
|
const handler = tearDown.bind(null, child);
|
|
|
|
process.on('exit', handler);
|
|
process.on('uncaughtException', handler);
|
|
process.on('SIGINT', handler);
|
|
};
|
|
|
|
exports.mainScriptSource = function() {
|
|
return fs.readFileSync(mainScript, 'utf8');
|
|
};
|