Skip to content

Commit 327bbdf

Browse files
authored
Merge pull request #5 from luminati-io/feature/stdio-transport
feat: Integrate stdio transport from upstream PR cyrus-and#440 (--remote-debugging-pipe)
2 parents 995f133 + c6a8480 commit 327bbdf

File tree

4 files changed

+191
-59
lines changed

4 files changed

+191
-59
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,8 +456,12 @@ Connects to a remote instance using the [Chrome Debugging Protocol].
456456
- `protocol`: [Chrome Debugging Protocol] descriptor object. Defaults to use the
457457
protocol chosen according to the `local` option;
458458
- `local`: a boolean indicating whether the protocol must be fetched *remotely*
459-
or if the local version must be used. It has no effect if the `protocol`
460-
option is set. Defaults to `false`.
459+
or if the local version must be used. It has no effect if the `protocol` or
460+
`process` option is set. Defaults to `false`.
461+
- `process`: a `ChildProcess` object that represents a Chrome instance launched
462+
with `--remote-debugging-pipe`. If passed, websocket-related options will be
463+
ignored and communications will occur over stdio instead. Note: the `protocol`
464+
cannot be fetched remotely if a `process` is passed.
461465
462466
These options are also valid properties of all the instances of the `CDP`
463467
class. In addition to that, the `webSocketUrl` field contains the currently used

lib/chrome.js

Lines changed: 87 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const WebSocket = require('ws');
1010
const api = require('./api.js');
1111
const defaults = require('./defaults.js');
1212
const devtools = require('./devtools.js');
13+
const StdioWrapper = require('./stdio-wrapper.js');
1314

1415
class ProtocolError extends Error {
1516
constructor(request, response) {
@@ -58,6 +59,7 @@ class Chrome extends EventEmitter {
5859
this.local = !!(options.local);
5960
this.target = options.target || defaultTarget;
6061
this.connectOptions = options.connectOptions;
62+
this.process = options.process;
6163
// locals
6264
this._notifier = notifier;
6365
this._callbacks = {};
@@ -92,7 +94,7 @@ class Chrome extends EventEmitter {
9294
const request = {method, params, sessionId};
9395
reject(
9496
error instanceof Error
95-
? error // low-level WebSocket error
97+
? error // low-level io error
9698
: new ProtocolError(request, response)
9799
);
98100
} else {
@@ -104,56 +106,52 @@ class Chrome extends EventEmitter {
104106
}
105107

106108
close(callback) {
107-
const closeWebSocket = (callback) => {
108-
// don't close if it's already closed
109-
if (this._ws.readyState === 3) {
110-
callback();
111-
} else {
112-
// don't notify on user-initiated shutdown ('disconnect' event)
113-
this._ws.removeAllListeners('close');
114-
this._ws.once('close', () => {
115-
this._ws.removeAllListeners();
116-
this._handleConnectionClose();
117-
callback();
118-
});
119-
this._ws.close();
120-
}
121-
};
122109
if (typeof callback === 'function') {
123-
closeWebSocket(callback);
110+
this._close(callback);
124111
return undefined;
125112
} else {
126113
return new Promise((fulfill, reject) => {
127-
closeWebSocket(fulfill);
114+
this._close(fulfill);
128115
});
129116
}
130117
}
131118

132119
// initiate the connection process
133120
async _start() {
134-
const options = {
135-
host: this.host,
136-
port: this.port,
137-
secure: this.secure,
138-
useHostName: this.useHostName,
139-
alterPath: this.alterPath,
140-
...this.connectOptions,
141-
};
142121
try {
143-
// fetch the WebSocket debugger URL
144-
const url = await this._fetchDebuggerURL(options);
145-
// allow the user to alter the URL
146-
const urlObject = parseUrl(url);
147-
urlObject.pathname = options.alterPath(urlObject.pathname);
148-
this.webSocketUrl = formatUrl(urlObject);
149-
// update the connection parameters using the debugging URL
150-
options.host = urlObject.hostname;
151-
options.port = urlObject.port || options.port;
152-
// fetch the protocol and prepare the API
153-
const protocol = await this._fetchProtocol(options);
154-
api.prepare(this, protocol);
155-
// finally connect to the WebSocket
156-
await this._connectToWebSocket();
122+
if (this.process)
123+
{
124+
// we first "connect" to stdio pipes, so that we can
125+
// first the protocol remotely via the pipe.
126+
await this._connect();
127+
const protocol = await this._fetchProtocol({});
128+
api.prepare(this, protocol);
129+
}
130+
else
131+
{
132+
const options = {
133+
host: this.host,
134+
port: this.port,
135+
secure: this.secure,
136+
useHostName: this.useHostName,
137+
alterPath: this.alterPath,
138+
...this.connectOptions,
139+
};
140+
// fetch the WebSocket debugger URL
141+
const url = await this._fetchWsDebuggerURL(options);
142+
// allow the user to alter the URL
143+
const urlObject = parseUrl(url);
144+
urlObject.pathname = options.alterPath(urlObject.pathname);
145+
this.webSocketUrl = formatUrl(urlObject);
146+
// update the connection parameters using the debugging URL
147+
options.host = urlObject.hostname;
148+
options.port = urlObject.port || options.port;
149+
// fetch the protocol and prepare the API
150+
const protocol = await this._fetchProtocol(options);
151+
api.prepare(this, protocol);
152+
// finally connect to the WebSocket
153+
await this._connect();
154+
}
157155
// since the handler is executed synchronously, the emit() must be
158156
// performed in the next tick so that uncaught errors in the client code
159157
// are not intercepted by the Promise mechanism and therefore reported
@@ -167,7 +165,7 @@ class Chrome extends EventEmitter {
167165
}
168166

169167
// fetch the WebSocket URL according to 'target'
170-
async _fetchDebuggerURL(options) {
168+
async _fetchWsDebuggerURL(options) {
171169
const userTarget = this.target;
172170
switch (typeof userTarget) {
173171
case 'string': {
@@ -212,40 +210,71 @@ class Chrome extends EventEmitter {
212210
// otherwise user either the local or the remote version
213211
else {
214212
options.local = this.local;
213+
if (this.process)
214+
options.cdp = this;
215215
return await devtools.Protocol(options);
216216
}
217217
}
218218

219-
// establish the WebSocket connection and start processing user commands
220-
_connectToWebSocket() {
219+
_createStdioWrapper() {
220+
const stdio = new StdioWrapper(this.process.stdio[3], this.process.stdio[4]);
221+
this._close = (...args)=>stdio.close(...args);
222+
this._send = (...args)=>stdio.send(...args);
223+
return stdio;
224+
}
225+
226+
_createWebSocketWrapper() {
227+
if (this.secure) {
228+
this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, 'wss:');
229+
}
230+
const ws = this._ws = new WebSocket(this.webSocketUrl, [], {
231+
followRedirects: true,
232+
...this.connectOptions,
233+
});
234+
this._close = (callback) => {
235+
// don't close if it's already closed
236+
if (ws.readyState === 3) {
237+
callback();
238+
} else {
239+
// don't notify on user-initiated shutdown ('disconnect' event)
240+
ws.removeAllListeners('close');
241+
ws.once('close', () => {
242+
ws.removeAllListeners();
243+
this._handleConnectionClose();
244+
callback();
245+
});
246+
ws.close();
247+
}
248+
};
249+
this._send = (...args)=>ws.send(...args);
250+
return ws;
251+
}
252+
253+
// establish the connection wrapper and start processing user commands
254+
_connect() {
221255
return new Promise((fulfill, reject) => {
222-
// create the WebSocket
223256
try {
224-
if (this.secure) {
225-
this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, 'wss:');
226-
}
227-
this._ws = new WebSocket(this.webSocketUrl, [], {
228-
followRedirects: true,
229-
...this.connectOptions
230-
});
257+
this._transport = this.process
258+
? this._createStdioWrapper()
259+
: this._createWebSocketWrapper();
231260
} catch (err) {
232-
// handles bad URLs
261+
// handle missing stdio streams, bad URLs...
233262
reject(err);
234263
return;
235264
}
236265
// set up event handlers
237-
this._ws.on('open', () => {
266+
this._transport.on('open', () => {
238267
fulfill();
239268
});
240-
this._ws.on('message', (data) => {
269+
this._transport.on('message', (data) => {
241270
const message = JSON.parse(data);
242271
this._handleMessage(message);
243272
});
244-
this._ws.on('close', (code) => {
273+
this._transport.on('close', (code) => {
245274
this._handleConnectionClose();
246275
this.emit('disconnect');
247276
});
248-
this._ws.on('error', (err) => {
277+
this._transport.on('error', (err) => {
249278
reject(err);
250279
});
251280
});
@@ -305,7 +334,7 @@ class Chrome extends EventEmitter {
305334
sessionId,
306335
params: params || {}
307336
};
308-
this._ws.send(JSON.stringify(message), (err) => {
337+
this._send(JSON.stringify(message), (err) => {
309338
if (err) {
310339
// handle low-level WebSocket errors
311340
if (typeof callback === 'function') {
@@ -317,6 +346,7 @@ class Chrome extends EventEmitter {
317346
}
318347
});
319348
}
349+
get transport() { return this._transport; }
320350
}
321351

322352
module.exports = Chrome;

lib/devtools.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ function promisesWrapper(func) {
4848
}
4949

5050
function Protocol(options, callback) {
51+
// fetch remotely via CDP when using stdio pipes (Bright Data exclusive)
52+
if (options.cdp)
53+
{
54+
return options.cdp.send('Browser.getProtocolJson')
55+
.then(data=>callback(null, JSON.parse(data.result)))
56+
.catch(callback);
57+
}
5158
// if the local protocol is requested
5259
if (options.local) {
5360
const localDescriptor = require('./protocol.json');

lib/stdio-wrapper.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict';
2+
3+
// Adapted from https://github.com/puppeteer/puppeteer/blob/7a2a41f2087b07e8ef1feaf3881bdcc3fd4922ca/src/PipeTransport.js
4+
5+
/**
6+
* Copyright 2018 Google Inc. All rights reserved.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
const { EventEmitter } = require('events');
22+
23+
function addEventListener(emitter, eventName, handler) {
24+
emitter.on(eventName, handler);
25+
return { emitter, eventName, handler };
26+
}
27+
28+
function removeEventListeners(listeners) {
29+
for (const listener of listeners)
30+
listener.emitter.removeListener(listener.eventName, listener.handler);
31+
listeners.length = 0;
32+
}
33+
34+
// wrapper for null-terminated stdio message transport
35+
class StdioWrapper extends EventEmitter {
36+
constructor(pipeWrite, pipeRead) {
37+
super();
38+
this._pipeWrite = pipeWrite;
39+
this._pendingMessage = '';
40+
this._eventListeners = [
41+
addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
42+
addEventListener(pipeRead, 'close', () => {
43+
this._pipeWrite = null;
44+
this.emit('close');
45+
}),
46+
addEventListener(pipeRead, 'error', (err) => this.emit('error', err)),
47+
addEventListener(pipeWrite, 'error', (err) => this.emit('error', err)),
48+
];
49+
setImmediate(() => this.emit('open'));
50+
}
51+
52+
send(message, callback) {
53+
try {
54+
if (!this._pipeWrite)
55+
throw new Error('CDP pipeWrite closed');
56+
this._pipeWrite.write(message);
57+
this._pipeWrite.write('\0');
58+
callback();
59+
} catch (err) {
60+
callback(err);
61+
}
62+
}
63+
64+
_dispatch(buffer) {
65+
let end = buffer.indexOf('\0');
66+
if (end === -1) {
67+
this._pendingMessage += buffer.toString();
68+
return;
69+
}
70+
const message = this._pendingMessage + buffer.toString(undefined, 0, end);
71+
72+
this.emit('message', message);
73+
74+
let start = end + 1;
75+
end = buffer.indexOf('\0', start);
76+
while (end !== -1) {
77+
this.emit('message', buffer.toString(undefined, start, end));
78+
start = end + 1;
79+
end = buffer.indexOf('\0', start);
80+
}
81+
this._pendingMessage = buffer.toString(undefined, start);
82+
}
83+
84+
close(callback) {
85+
this._pipeWrite = null;
86+
removeEventListeners(this._eventListeners);
87+
callback();
88+
}
89+
}
90+
91+
module.exports = StdioWrapper;

0 commit comments

Comments
 (0)