Skip to content

Commit 9745f92

Browse files
flotwigkevzoid
authored andcommitted
feat: support for --remote-debugging-pipe transport
1 parent 995f133 commit 9745f92

File tree

3 files changed

+155
-47
lines changed

3 files changed

+155
-47
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: 62 additions & 45 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) {
@@ -55,9 +56,10 @@ class Chrome extends EventEmitter {
5556
this.useHostName = !!(options.useHostName);
5657
this.alterPath = options.alterPath || ((path) => path);
5758
this.protocol = options.protocol;
58-
this.local = !!(options.local);
59+
this.local = !!(options.local || options.process);
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 = {};
@@ -104,27 +106,12 @@ 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
}
@@ -140,20 +127,22 @@ class Chrome extends EventEmitter {
140127
...this.connectOptions,
141128
};
142129
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;
130+
if (!this.process) {
131+
// fetch the WebSocket debugger URL
132+
const url = await this._fetchDebuggerURL(options);
133+
// allow the user to alter the URL
134+
const urlObject = parseUrl(url);
135+
urlObject.pathname = options.alterPath(urlObject.pathname);
136+
this.webSocketUrl = formatUrl(urlObject);
137+
// update the connection parameters using the debugging URL
138+
options.host = urlObject.hostname;
139+
options.port = urlObject.port || options.port;
140+
}
152141
// fetch the protocol and prepare the API
153142
const protocol = await this._fetchProtocol(options);
154143
api.prepare(this, protocol);
155-
// finally connect to the WebSocket
156-
await this._connectToWebSocket();
144+
// finally connect to the WebSocket or stdio
145+
await this._connect();
157146
// since the handler is executed synchronously, the emit() must be
158147
// performed in the next tick so that uncaught errors in the client code
159148
// are not intercepted by the Promise mechanism and therefore reported
@@ -216,36 +205,64 @@ class Chrome extends EventEmitter {
216205
}
217206
}
218207

219-
// establish the WebSocket connection and start processing user commands
220-
_connectToWebSocket() {
208+
_createStdioWrapper() {
209+
const stdio = new StdioWrapper(this.process.stdio[3], this.process.stdio[4]);
210+
this._close = stdio.close.bind(stdio);
211+
this._send = stdio.send.bind(stdio);
212+
return stdio;
213+
}
214+
215+
_createWebSocketWrapper() {
216+
if (this.secure) {
217+
this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, 'wss:');
218+
}
219+
const ws = new WebSocket(this.webSocketUrl, [], {
220+
followRedirects: true,
221+
...this.connectOptions,
222+
});
223+
this._close = (callback) => {
224+
// don't close if it's already closed
225+
if (ws.readyState === 3) {
226+
callback();
227+
} else {
228+
// don't notify on user-initiated shutdown ('disconnect' event)
229+
ws.removeAllListeners('close');
230+
ws.once('close', () => {
231+
ws.removeAllListeners();
232+
this._handleConnectionClose();
233+
callback();
234+
});
235+
ws.close();
236+
}
237+
};
238+
this._send = ws.send.bind(ws);
239+
return ws;
240+
}
241+
242+
// establish the connection wrapper and start processing user commands
243+
_connect() {
221244
return new Promise((fulfill, reject) => {
222-
// create the WebSocket
245+
let wrapper;
223246
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-
});
247+
wrapper = this.process ? this._createStdioWrapper() : this._createWebSocketWrapper();
231248
} catch (err) {
232-
// handles bad URLs
249+
// handle missing stdio streams, bad URLs...
233250
reject(err);
234251
return;
235252
}
236253
// set up event handlers
237-
this._ws.on('open', () => {
254+
wrapper.on('open', () => {
238255
fulfill();
239256
});
240-
this._ws.on('message', (data) => {
257+
wrapper.on('message', (data) => {
241258
const message = JSON.parse(data);
242259
this._handleMessage(message);
243260
});
244-
this._ws.on('close', (code) => {
261+
wrapper.on('close', (code) => {
245262
this._handleConnectionClose();
246263
this.emit('disconnect');
247264
});
248-
this._ws.on('error', (err) => {
265+
wrapper.on('error', (err) => {
249266
reject(err);
250267
});
251268
});
@@ -305,7 +322,7 @@ class Chrome extends EventEmitter {
305322
sessionId,
306323
params: params || {}
307324
};
308-
this._ws.send(JSON.stringify(message), (err) => {
325+
this._send(JSON.stringify(message), (err) => {
309326
if (err) {
310327
// handle low-level WebSocket errors
311328
if (typeof callback === 'function') {

lib/stdio-wrapper.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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', () => this.emit('close')),
43+
addEventListener(pipeRead, 'error', (err) => this.emit('error', err)),
44+
addEventListener(pipeWrite, 'error', (err) => this.emit('error', err)),
45+
];
46+
process.nextTick(() => {
47+
this.emit('open');
48+
});
49+
}
50+
51+
send(message, callback) {
52+
try {
53+
this._pipeWrite.write(message);
54+
this._pipeWrite.write('\0');
55+
callback();
56+
} catch (err) {
57+
callback(err);
58+
}
59+
}
60+
61+
_dispatch(buffer) {
62+
let end = buffer.indexOf('\0');
63+
if (end === -1) {
64+
this._pendingMessage += buffer.toString();
65+
return;
66+
}
67+
const message = this._pendingMessage + buffer.toString(undefined, 0, end);
68+
69+
this.emit('message', message);
70+
71+
let start = end + 1;
72+
end = buffer.indexOf('\0', start);
73+
while (end !== -1) {
74+
this.emit('message', buffer.toString(undefined, start, end));
75+
start = end + 1;
76+
end = buffer.indexOf('\0', start);
77+
}
78+
this._pendingMessage = buffer.toString(undefined, start);
79+
}
80+
81+
close() {
82+
this._pipeWrite = null;
83+
removeEventListeners(this._eventListeners);
84+
}
85+
}
86+
87+
module.exports = StdioWrapper;

0 commit comments

Comments
 (0)