Skip to content
This repository was archived by the owner on Feb 1, 2022. It is now read-only.

Commit b39b3bc

Browse files
author
Jan Krems
committed
feat: Launch child
1 parent ba060d3 commit b39b3bc

4 files changed

Lines changed: 394 additions & 250 deletions

File tree

lib/internal/inspect-protocol.js

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/*
2+
* Copyright Node.js contributors. All rights reserved.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to
6+
* deal in the Software without restriction, including without limitation the
7+
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8+
* sell copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20+
* IN THE SOFTWARE.
21+
*/
22+
'use strict';
23+
const { EventEmitter } = require('events');
24+
const http = require('http');
25+
const util = require('util');
26+
27+
const debuglog = util.debuglog('inspect');
28+
29+
const kOpCodeText = 0x1;
30+
const kOpCodeClose = 0x8;
31+
32+
const kFinalBit = 0x80;
33+
const kReserved1Bit = 0x40;
34+
const kReserved2Bit = 0x20;
35+
const kReserved3Bit = 0x10;
36+
const kOpCodeMask = 0xF;
37+
const kMaskBit = 0x80;
38+
const kPayloadLengthMask = 0x7F;
39+
40+
const kMaxSingleBytePayloadLength = 125;
41+
const kMaxTwoBytePayloadLength = 0xFFFF;
42+
const kTwoBytePayloadLengthField = 126;
43+
const kEightBytePayloadLengthField = 127;
44+
const kMaskingKeyWidthInBytes = 4;
45+
46+
function ignoreError() {}
47+
48+
function isEmpty(obj) {
49+
return Object.keys(obj).length === 0;
50+
}
51+
52+
function unpackError({ code, message, data }) {
53+
const err = new Error(`${message} - ${data}`);
54+
err.code = code;
55+
Error.captureStackTrace(err, unpackError);
56+
return err;
57+
}
58+
59+
function encodeFrameHybi17(payload) {
60+
const dataLength = payload.length;
61+
62+
let singleByteLength;
63+
let additionalLength;
64+
if (dataLength > kMaxTwoBytePayloadLength) {
65+
singleByteLength = kEightBytePayloadLengthField;
66+
additionalLength = new Buffer(8);
67+
let remaining = dataLength;
68+
for (let i = 0; i < 8; ++i) {
69+
additionalLength[7 - i] = remaining & 0xFF;
70+
remaining >>= 8;
71+
}
72+
} else if (dataLength > kMaxSingleBytePayloadLength) {
73+
singleByteLength = kTwoBytePayloadLengthField;
74+
additionalLength = new Buffer(2);
75+
additionalLength[0] = (dataLength & 0xFF00) >> 8;
76+
additionalLength[1] = dataLength & 0xFF;
77+
} else {
78+
additionalLength = new Buffer(0);
79+
singleByteLength = dataLength;
80+
}
81+
82+
const header = new Buffer([
83+
kFinalBit | kOpCodeText,
84+
kMaskBit | singleByteLength,
85+
]);
86+
87+
const mask = new Buffer(4);
88+
const masked = new Buffer(dataLength);
89+
for (let i = 0; i < dataLength; ++i) {
90+
masked[i] = payload[i] ^ mask[i % kMaskingKeyWidthInBytes];
91+
}
92+
93+
return Buffer.concat([header, additionalLength, mask, masked]);
94+
}
95+
96+
function decodeFrameHybi17(data) {
97+
const dataAvailable = data.length;
98+
const notComplete = { closed: false, payload: null, rest: data };
99+
let payloadOffset = 2;
100+
if ((dataAvailable - payloadOffset) < 0) return notComplete;
101+
102+
const firstByte = data[0];
103+
const secondByte = data[1];
104+
105+
const final = (firstByte & kFinalBit) !== 0;
106+
const reserved1 = (firstByte & kReserved1Bit) !== 0;
107+
const reserved2 = (firstByte & kReserved2Bit) !== 0;
108+
const reserved3 = (firstByte & kReserved3Bit) !== 0;
109+
const opCode = firstByte & kOpCodeMask;
110+
const masked = (secondByte & kMaskBit) !== 0;
111+
const compressed = reserved1;
112+
if (compressed) {
113+
throw new Error('Compressed frames not supported');
114+
}
115+
if (!final || reserved2 || reserved3) {
116+
throw new Error('Only compression extension is supported');
117+
}
118+
119+
if (masked) {
120+
throw new Error('Masked server frame - not supported');
121+
}
122+
123+
let closed = false;
124+
switch (opCode) {
125+
case kOpCodeClose:
126+
closed = true;
127+
break;
128+
case kOpCodeText:
129+
break;
130+
default:
131+
throw new Error(`Unsupported op code ${opCode}`);
132+
}
133+
134+
let payloadLength = secondByte & kPayloadLengthMask;
135+
switch (payloadLength) {
136+
case kTwoBytePayloadLengthField:
137+
payloadOffset += 2;
138+
payloadLength = (data[2] << 8) + data[3];
139+
break;
140+
141+
case kEightBytePayloadLengthField:
142+
payloadOffset += 8;
143+
payloadLength = 0;
144+
for (let i = 0; i < 8; ++i) {
145+
payloadLength <<= 8;
146+
payloadLength |= data[2 + i];
147+
}
148+
break;
149+
150+
default:
151+
// Nothing. We already have the right size.
152+
}
153+
if ((dataAvailable - payloadOffset) < 0) return notComplete;
154+
155+
const payloadEnd = payloadOffset + payloadLength;
156+
return {
157+
payload: data.slice(payloadOffset, payloadEnd),
158+
rest: data.slice(payloadEnd),
159+
closed,
160+
};
161+
}
162+
163+
class Client extends EventEmitter {
164+
constructor(port, host) {
165+
super();
166+
this.handleChunk = this._handleChunk.bind(this);
167+
168+
this._port = port;
169+
this._host = host;
170+
171+
this.reset();
172+
}
173+
174+
_handleChunk(chunk) {
175+
this._unprocessed = Buffer.concat([this._unprocessed, chunk]);
176+
177+
while (this._unprocessed.length > 2) {
178+
const { payload, rest } = decodeFrameHybi17(this._unprocessed);
179+
this._unprocessed = rest;
180+
if (payload === null) break;
181+
182+
debuglog('< %s', payload);
183+
const { id, method, params, result, error } = JSON.parse(payload.toString());
184+
if (id) {
185+
const handler = this._pending[id];
186+
if (handler) {
187+
delete this._pending[id];
188+
handler(error, result);
189+
}
190+
} else if (method) {
191+
this.emit('debugEvent', method, params);
192+
this.emit(method, params);
193+
} else {
194+
throw new Error(`Unsupported response: ${payload.toString()}`);
195+
}
196+
}
197+
}
198+
199+
reset() {
200+
if (this._http) {
201+
this._http.destroy();
202+
}
203+
this._http = null;
204+
this._open = false;
205+
this._lastId = 0;
206+
this._socket = null;
207+
this._pending = {};
208+
this._unprocessed = new Buffer(0);
209+
}
210+
211+
callMethod(method, params) {
212+
return new Promise((resolve, reject) => {
213+
const data = { id: ++this._lastId, method, params };
214+
this._pending[data.id] = (error, result) => {
215+
if (error) reject(unpackError(error));
216+
else resolve(isEmpty(result) ? undefined : result);
217+
};
218+
const json = JSON.stringify(data);
219+
debuglog('> %s', json);
220+
this._socket.write(encodeFrameHybi17(new Buffer(json)));
221+
});
222+
}
223+
224+
connect() {
225+
this.reset();
226+
227+
const key1 = crypto.randomBytes(16).toString('base64');
228+
229+
const httpReq = this._http = http.request({
230+
host: this._host,
231+
port: this._port,
232+
path: '/node',
233+
headers: {
234+
Connection: 'Upgrade',
235+
Upgrade: 'websocket',
236+
'Sec-WebSocket-Key': key1,
237+
},
238+
});
239+
httpReq.on('error', e => {
240+
this.emit('error', e);
241+
});
242+
httpReq.on('response', httpRes => {
243+
if (httpRes.statusCode >= 400) {
244+
httpRes.pipe(process.stderr);
245+
} else {
246+
httpRes.pipe(process.stderr);
247+
}
248+
});
249+
250+
const handshakeListener = (res, socket) => {
251+
// TODO: we *could* validate res.headers[sec-websocket-accept]
252+
253+
this._socket = socket;
254+
socket.on('data', this.handleChunk);
255+
256+
Promise.all([
257+
this.callMethod('Log.enable').then(null, ignoreError),
258+
this.callMethod('Runtime.enable'),
259+
this.callMethod('Page.enable').then(null, ignoreError),
260+
this.callMethod('Page.getResourceTree').then(null, ignoreError),
261+
this.callMethod('Debugger.enable'),
262+
this.callMethod('Debugger.setPauseOnExceptions', { state: 'none' }),
263+
this.callMethod('Debugger.setAsyncCallStackDepth', { maxDepth: 0 }),
264+
this.callMethod('Profiler.enable'),
265+
this.callMethod('Profiler.setSamplingInterval', { interval: 100 }),
266+
this.callMethod('Debugger.setBlackboxPatterns', { patterns: [] }),
267+
this.callMethod('Runtime.run'),
268+
]).then(() => {
269+
this.emit('ready');
270+
}, error => {
271+
this.emit('error', error);
272+
});
273+
};
274+
httpReq.on('upgrade', handshakeListener);
275+
httpReq.end();
276+
}
277+
}
278+
module.exports = Client;

0 commit comments

Comments
 (0)