-
Notifications
You must be signed in to change notification settings - Fork 231
Feat/network #282
base: master
Are you sure you want to change the base?
Feat/network #282
Changes from 10 commits
3ac3d9c
04f071a
d357732
33e6242
1ed024a
be3c73c
9b51ded
77b7cfe
f99b51a
3d7113d
db2de6e
74ae0e5
971761b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||
*/ | ||
|
||
(function(){ | ||
(async function(){ | ||
InspectorFrontendHost.getPreferences = async function(callback) { | ||
[Ndb.backend] = await carlo.loadParams(); | ||
const prefs = { | ||
|
@@ -30,4 +30,53 @@ | |
callback({statusCode: 404}); | ||
} | ||
}; | ||
|
||
InspectorFrontendHost.sendMessageToBackend = async rawMessage => { | ||
const parsedMes = JSON.parse(rawMessage); | ||
if (parsedMes.method !== 'Network.getResponseBody') | ||
return; | ||
|
||
const mes = await target.runtimeAgent().invoke_evaluate({ | ||
expression: `process._sendMessage(${JSON.stringify(JSON.parse(rawMessage))})`, | ||
awaitPromise: true | ||
}); | ||
|
||
if (!mes.result) return; | ||
try { | ||
const [id, result] = mes.result.value; | ||
if (result) { | ||
InspectorFrontendHost.events.dispatchEventToListeners( | ||
InspectorFrontendHostAPI.Events.DispatchMessage, | ||
{ | ||
id, | ||
result | ||
} | ||
); | ||
} | ||
} catch (err) { | ||
console.log(err); | ||
} | ||
}; | ||
|
||
while (true) { | ||
const message = await target.runtimeAgent().invoke_evaluate({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ... and here as well? |
||
expression: 'process._getNetworkMessages()', | ||
awaitPromise: true | ||
}); | ||
|
||
if (!message.result) return; | ||
const arrMessages = JSON.parse(message.result.value); | ||
|
||
for (const mes of arrMessages) { | ||
const { type, payload } = mes; | ||
|
||
if (type) { | ||
SDK._mainConnection._onMessage(JSON.stringify({ | ||
method: type, | ||
params: payload | ||
})); | ||
} | ||
} | ||
} | ||
|
||
})(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
const zlib = require('zlib'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 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; | ||
}; | ||
|
||
let cacheRequests = {}; | ||
let id = 1; | ||
const getId = () => id++; | ||
|
||
let messages = []; | ||
let messageAdded = null; | ||
|
||
function reportMessage(message) { | ||
messages.push(message); | ||
if (messageAdded) { | ||
setTimeout(messageAdded, 0); | ||
messageAdded = null; | ||
} | ||
} | ||
|
||
process._getNetworkMessages = async function() { | ||
if (!messages.length) | ||
await new Promise(resolve => messageAdded = resolve); | ||
return JSON.stringify(messages.splice(0)); | ||
}; | ||
|
||
process._sendMessage = async function(rawMessage) { | ||
return new Promise(resolve => { | ||
const message = rawMessage; | ||
if (!cacheRequests[message.params.requestId]) { | ||
resolve(JSON.stringify({})); | ||
} else { | ||
if (message.method === 'Network.getResponseBody') { | ||
const { base64Encoded, data } = cacheRequests[message.params.requestId]; | ||
|
||
console.log({ cacheRequests }); | ||
console.log({ data }); | ||
resolve(JSON.stringify([message.id, { base64Encoded, body: data }])); | ||
} | ||
} | ||
}); | ||
}; | ||
|
||
const callbackWrapper = (callback, req) => res => { | ||
const requestId = getId(); | ||
res.req.__requestId = requestId; | ||
|
||
reportMessage({ | ||
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. | ||
reportMessage({ payload: payload, type: 'Network.responseReceived' }); | ||
reportMessage({ 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; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ak239 how can I have
target.runtimeAgent()
here?...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that we need some help from @josepharhar. We need to find some nicer way to avoid hacking network model.
In theory we can register our own network model, that will implement most stuff on top of runtime agent. Please take a look on
NodeWorker
orNodeRuntime
models. There are examples how we can add custom models, in this case we will add our own NetworkModel implementation and I hope that DevTools frontend will use it as is.@josepharhar do you have any suggestion how we can do it better and will it work if node target will just have own NetworkModel?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@khanghoang could you rebase your change on top of latest master? It has separated
Ndb.Connection
class. Alternative idea that will work to me, addNdb.Connection.addInterceptor({sendRawMessage: function(string):boolean})
. If interceptor is added thanNdb.Connection.sendRawMessage
first tries to process message using one of interceptor and iff all interceptors return false will send this message tothis._channel
.For
Network
you can add network interceptor inNdb.NodeProcessManager.detected
, something like:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think having our own NetworkManager/NetworkModel for node sounds like a great idea, and seems like a step in the right direction of eventually forking the entire network panel. If I understand correctly, this means that instead of implementing the CDP interface of Network, we will instead be implementing the public methods of SDK.NetworkManager and emitting the events it emits in order to talk to the network panel. This sounds good to me because its closer to the network panel itself and is probably simpler. I'm not sure exactly how to do this within ndb, but I'm sure you know how @ak239
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@khanghoang I created proof of concept of network interceptor - feel free to use it - I hope that it covers your use case - 5aef915
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ak239 @josepharhar thanks a lot. I will check it.