Skip to content
This repository has been archived by the owner on Jul 28, 2023. It is now read-only.

Feat/network #282

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
28 changes: 17 additions & 11 deletions front_end/ndb.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
{
"modules" : [
{ "name": "ndb_sdk", "type": "autostart" },
{ "name": "ndb", "type": "autostart" },
{ "name": "layer_viewer" },
{ "name": "timeline_model" },
{ "name": "timeline" },
{ "name": "product_registry" },
{ "name": "mobile_throttling" },
{ "name": "ndb_ui" },
{ "name": "xterm" }
],
"modules": [
{ "name": "ndb_sdk", "type": "autostart" },
{ "name": "ndb", "type": "autostart" },
{ "name": "layer_viewer" },
{ "name": "timeline_model" },
{ "name": "timeline" },
{ "name": "product_registry" },
{ "name": "mobile_throttling" },
{ "name": "ndb_ui" },
{ "name": "xterm" },
{ "name": "emulation", "type": "autostart" },
{ "name": "inspector_main", "type": "autostart" },
{ "name": "mobile_throttling", "type": "autostart" },
{ "name": "cookie_table" },
{ "name": "har_importer" },
{ "name": "network" }
],
"extends": "shell",
"has_html": true
}
96 changes: 79 additions & 17 deletions front_end/ndb/NdbMain.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@
*/

Ndb.nodeExecPath = function() {
if (!Ndb._nodeExecPathPromise)
Ndb._nodeExecPathPromise = Ndb.backend.which('node').then(result => result.resolvedPath);
if (!Ndb._nodeExecPathPromise) {
Copy link
Contributor

Choose a reason for hiding this comment

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

please undo these formatting changes, it will simplify review.

Ndb._nodeExecPathPromise = Ndb.backend
.which('node')
.then(result => result.resolvedPath);
}
return Ndb._nodeExecPathPromise;
};

Ndb.npmExecPath = function() {
if (!Ndb._npmExecPathPromise)
Ndb._npmExecPathPromise = Ndb.backend.which('npm').then(result => result.resolvedPath);
if (!Ndb._npmExecPathPromise) {
Ndb._npmExecPathPromise = Ndb.backend
.which('npm')
.then(result => result.resolvedPath);
}
return Ndb._npmExecPathPromise;
};

Expand All @@ -27,39 +33,60 @@ Ndb.processInfo = function() {
*/
Ndb.NdbMain = class extends Common.Object {
/**
* @override
*/
* @override
*/
async run() {
InspectorFrontendAPI.setUseSoftMenu(true);
document.title = 'ndb';
Common.moduleSetting('blackboxInternalScripts').addChangeListener(Ndb.NdbMain._calculateBlackboxState);
Common.moduleSetting('blackboxInternalScripts').addChangeListener(
Ndb.NdbMain._calculateBlackboxState
);
Ndb.NdbMain._calculateBlackboxState();

const setting = Persistence.isolatedFileSystemManager.workspaceFolderExcludePatternSetting();
setting.set(Ndb.NdbMain._defaultExcludePattern().join('|'));
Ndb.nodeProcessManager = await Ndb.NodeProcessManager.create(SDK.targetManager);
Ndb.nodeProcessManager = await Ndb.NodeProcessManager.create(
SDK.targetManager
);

const {cwd} = await Ndb.processInfo();
const { cwd } = await Ndb.processInfo();
await Ndb.nodeProcessManager.addFileSystem(cwd);

// TODO(ak239): we do not want to create this model for workers, so we need a way to add custom capabilities.
SDK.SDKModel.register(NdbSdk.NodeWorkerModel, SDK.Target.Capability.JS, true);
SDK.SDKModel.register(NdbSdk.NodeRuntimeModel, SDK.Target.Capability.JS, true);
SDK.SDKModel.register(
NdbSdk.NodeWorkerModel,
SDK.Target.Capability.JS,
true
);
SDK.SDKModel.register(
NdbSdk.NodeRuntimeModel,
SDK.Target.Capability.JS,
true
);

await new Promise(resolve => SDK.initMainConnection(resolve));
SDK.targetManager.createTarget('<root>', ls`Root`, SDK.Target.Type.Browser, null);
SDK.targetManager.createTarget(
'<root>',
ls`Root`,
SDK.Target.Type.Browser,
null
);

if (Common.moduleSetting('autoStartMain').get()) {
const main = await Ndb.mainConfiguration();
if (main) {
if (main.prof)
await Ndb.nodeProcessManager.profile(main.execPath, main.args);
else
Ndb.nodeProcessManager.debug(main.execPath, main.args);
if (main.prof) {
await Ndb.nodeProcessManager.profile(
main.execPath,
main.args
);
} else {Ndb.nodeProcessManager.debug(main.execPath, main.args);}
}
}
Ndb.nodeProcessManager.startRepl();
}


static _defaultExcludePattern() {
const defaultCommonExcludedFolders = [
'/bower_components/', '/\\.devtools', '/\\.git/', '/\\.sass-cache/', '/\\.hg/', '/\\.idea/',
Expand Down Expand Up @@ -125,7 +152,6 @@ Ndb.mainConfiguration = async() => {
prof
};
};

/**
* @implements {UI.ContextMenu.Provider}
* @unrestricted
Expand Down Expand Up @@ -172,9 +198,21 @@ Ndb.NodeProcessManager = class extends Common.Object {
static async create(targetManager) {
const manager = new Ndb.NodeProcessManager(targetManager);
manager._service = await Ndb.backend.createService('ndd_service.js', rpc.handle(manager));
InspectorFrontendHost.sendMessageToBackend = manager.sendMessageToBackend.bind(manager);
Copy link
Contributor

Choose a reason for hiding this comment

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

This line was removed in master and I believe that you can remove it..


return manager;
}

/**
* @param {object} message
*
* @return {Promise} void
*/
async sendMessageToBackend(message) {
Copy link
Contributor

Choose a reason for hiding this comment

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

.. and this function.

if (this._service && this._service.sendMessage)
return this._service.sendMessage(message);
}

env() {
return this._service.env();
}
Expand Down Expand Up @@ -246,6 +284,30 @@ Ndb.NodeProcessManager = class extends Common.Object {
}
}

sendLoadingFinished({ type, payload }) {
SDK._mainConnection._onMessage(JSON.stringify({
method: 'Network.loadingFinished',
params: payload
}));
}

responseToFrontEnd(id, result) {
InspectorFrontendHost.events.dispatchEventToListeners(
InspectorFrontendHostAPI.Events.DispatchMessage,
{
id,
result
}
);
}

sendNetworkData({ type, payload }) {
SDK._mainConnection._onMessage(JSON.stringify({
method: type,
params: payload
}));
}

async terminalData(stream, data) {
const content = await(await fetch(`data:application/octet-stream;base64,${data}`)).text();
if (content.startsWith('Debugger listening on') || content.startsWith('Debugger attached.') || content.startsWith('Waiting for the debugger to disconnect...'))
Expand Down
9 changes: 8 additions & 1 deletion front_end/ndb/module.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@
"className": "Ndb.ContextMenuProvider"
}
],
"dependencies": ["common", "sdk", "ndb_sdk", "bindings", "persistence", "components"],
"dependencies": [
"common",
"sdk",
"ndb_sdk",
"bindings",
"persistence",
"components"
],
"scripts": [
"InspectorFrontendHostOverrides.js",
"Connection.js",
Expand Down
153 changes: 153 additions & 0 deletions lib/preload/ndb/httpMonkeyPatching.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
const zlib = require('zlib');
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to use another way to inject this script, and another channel for inspector node process to speak with DevTools frontend. We already have channel between node process and frontend - devtools protocol.

To inject this script please do following:

  • add httpMonkeyPatchingSource method to backend.js, this method returns source of this file.
  • Ndb.NodeProcessManager.detected method gets source of this script and inject it to the page using following snippet:
target.runtimeAgent().invoke_evaluate({
  expression: await Ndb.backend.httpMonkeyPatchingSource()
});

After these steps we can inject monkey patching script to any inspected process. Second step is how we can build a channel. We can use Runtime.evaluate with awaitPromise: true flag to build a channel. Monkey patching script gets following code:

let messages = [];
let messageAdded = null;

// this function is our instrumentation, to report anything to frontend - call it instead of process.send
function reportMessage(message) {
  messages.push(message);
  if (messageAdded) {
    setTimeout(messageAdded, 0);
    messageAdded = null;
  }
}

// this function should be called from frontend in the loop using `target.runtimeAgent().invoke_evaluate`
process._getNetworkMessages = async function() {
    if (!messages.length)
      await new Promise(resolve => messageAdded = resolve);
    return messages.splice(0);
  }
}

Frontend calls in the loop following code:

while (true) {
  const messages = await target.runtimeAgent().invoke_evaluate({
    expression: 'process._getNetworkMessages()', awaitPromise: true
  });
  // ... process these messages ...
}

Feel free to ask any questions! At the same time we can merge your pull request and I will refactor it.

const http = require('http');
const https = require('https');

const initTime = process.hrtime();

// DT requires us to use relative time in a strange format (xxx.xxx)
const getTime = () => {
const diff = process.hrtime(initTime);

return diff[0] + diff[1] / 1e9;
};

const formatRequestHeaders = req => {
if (!req.headers) return {};
return Object.keys(req.headers).reduce((acc, k) => {
if (typeof req.headers[k] === 'string') acc[k] = req.headers[k];
return acc;
}, {});
};

const formatResponseHeaders = res => {
if (!res.headers) return {};
return Object.keys(res.headers).reduce((acc, k) => {
if (typeof res.headers[k] === 'string') acc[k] = res.headers[k];
return acc;
}, {});
};

const getMineType = mimeType => {
// nasty hack for ASF
if (mimeType === 'OPENJSON')
return 'application/json;charset=UTF-8';


return mimeType;
};

const cacheRequests = {};
let id = 1;
const getId = () => id++;

const callbackWrapper = (callback, req) => res => {
const requestId = getId();
res.req.__requestId = requestId;

process.send({
payload: {
requestId: requestId,
loaderId: requestId,
documentURL: req.href,
request: {
url: req.href,
method: req.method,
headers: formatRequestHeaders(req),
mixedContentType: 'none',
initialPriority: 'VeryHigh',
referrerPolicy: 'no-referrer-when-downgrade',
postData: req.body
},
timestamp: getTime(),
wallTime: Date.now(),
initiator: {
type: 'other'
},
type: 'Document'
},
type: 'Network.requestWillBeSent'
});

const encoding = res.headers['content-encoding'];
let rawData = [];

const onEnd = function() {
rawData = Buffer.concat(rawData);
rawData = rawData.toString('base64');

cacheRequests[res.req.__requestId] = {
...res,
__rawData: rawData,
base64Encoded: true
};
const payload = {
id: res.req.__requestId,
requestId: res.req.__requestId,
loaderId: res.req.__requestId,
base64Encoded: true,
data: cacheRequests[res.req.__requestId].__rawData,
timestamp: getTime(),
type: 'XHR',
encodedDataLength: 100,
response: {
url: req.href,
status: res.statusCode,
statusText: res.statusText,
// set-cookie prop in the header has value as an array
// for example: ["__cfduid=dbfe006ef71658bf4dba321343c227f9a15449556…20:29 GMT; path=/; domain=.typicode.com; HttpOnly"]
headers: formatResponseHeaders(res),
mimeType: getMineType(
res.headers['content-encoding'] ||
res.headers['content-type']
),
requestHeaders: formatRequestHeaders(req)
}
};

// Send the response back.
process.send({ payload: payload, type: 'Network.responseReceived' });
process.send({ payload: payload, type: 'Network.loadingFinished' });
};

if (encoding === 'gzip' || encoding === 'x-gzip') {
const gunzip = zlib.createGunzip();
res.pipe(gunzip);

gunzip.on('data', function(data) {
rawData.push(data);
});
gunzip.on('end', onEnd);
} else {
res.on('data', chunk => {
rawData.push(chunk);
});
res.on('end', onEnd);
}

callback && callback(res);
};

const originHTTPRequest = http.request;
http.request = function wrapMethodRequest(req, callback) {
const request = originHTTPRequest.call(
this,
req,
callbackWrapper(callback, req)
);
return request;
};

const originHTTPSRequest = https.request;
https.request = function wrapMethodRequest(req, callback) {
const request = originHTTPSRequest.call(
this,
req,
callbackWrapper(callback, req)
);
const originWrite = request.write.bind(request);
request.write = data => {
req.body = data.toString();
originWrite(data);
};
return request;
};
Loading