Skip to content

Commit baad1bd

Browse files
committed
feat: support for --remote-debugging-pipe transport
1 parent b354aa1 commit baad1bd

File tree

3 files changed

+151
-43
lines changed

3 files changed

+151
-43
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,8 +439,12 @@ Connects to a remote instance using the [Chrome Debugging Protocol].
439439
- `protocol`: [Chrome Debugging Protocol] descriptor object. Defaults to use the
440440
protocol chosen according to the `local` option;
441441
- `local`: a boolean indicating whether the protocol must be fetched *remotely*
442-
or if the local version must be used. It has no effect if the `protocol`
443-
option is set. Defaults to `false`.
442+
or if the local version must be used. It has no effect if the `protocol` or
443+
`process` option is set. Defaults to `false`.
444+
- `process`: a `ChildProcess` object that represents a Chrome instance launched
445+
with `--remote-debugging-pipe`. If passed, websocket-related options will be
446+
ignored and communications will occur over stdio instead. Note: the `protocol`
447+
cannot be fetched remotely if a `process` is passed.
444448

445449
These options are also valid properties of all the instances of the `CDP`
446450
class. In addition to that, the `webSocketUrl` field contains the currently used

lib/chrome.js

Lines changed: 58 additions & 41 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,8 +56,9 @@ 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;
61+
this.process = options.process;
6062
// locals
6163
this._notifier = notifier;
6264
this._callbacks = {};
@@ -101,26 +103,12 @@ class Chrome extends EventEmitter {
101103
}
102104

103105
close(callback) {
104-
const closeWebSocket = (callback) => {
105-
// don't close if it's already closed
106-
if (this._ws.readyState === 3) {
107-
callback();
108-
} else {
109-
// don't notify on user-initiated shutdown ('disconnect' event)
110-
this._ws.removeAllListeners('close');
111-
this._ws.once('close', () => {
112-
this._ws.removeAllListeners();
113-
callback();
114-
});
115-
this._ws.close();
116-
}
117-
};
118106
if (typeof callback === 'function') {
119-
closeWebSocket(callback);
107+
this._close(callback);
120108
return undefined;
121109
} else {
122110
return new Promise((fulfill, reject) => {
123-
closeWebSocket(fulfill);
111+
this._close(fulfill);
124112
});
125113
}
126114
}
@@ -135,20 +123,22 @@ class Chrome extends EventEmitter {
135123
alterPath: this.alterPath
136124
};
137125
try {
138-
// fetch the WebSocket debugger URL
139-
const url = await this._fetchDebuggerURL(options);
140-
// allow the user to alter the URL
141-
const urlObject = parseUrl(url);
142-
urlObject.pathname = options.alterPath(urlObject.pathname);
143-
this.webSocketUrl = formatUrl(urlObject);
144-
// update the connection parameters using the debugging URL
145-
options.host = urlObject.hostname;
146-
options.port = urlObject.port || options.port;
126+
if (!this.process) {
127+
// fetch the WebSocket debugger URL
128+
const url = await this._fetchDebuggerURL(options);
129+
// allow the user to alter the URL
130+
const urlObject = parseUrl(url);
131+
urlObject.pathname = options.alterPath(urlObject.pathname);
132+
this.webSocketUrl = formatUrl(urlObject);
133+
// update the connection parameters using the debugging URL
134+
options.host = urlObject.hostname;
135+
options.port = urlObject.port || options.port;
136+
}
147137
// fetch the protocol and prepare the API
148138
const protocol = await this._fetchProtocol(options);
149139
api.prepare(this, protocol);
150-
// finally connect to the WebSocket
151-
await this._connectToWebSocket();
140+
// finally connect to the WebSocket or stdio
141+
await this._connect();
152142
// since the handler is executed synchronously, the emit() must be
153143
// performed in the next tick so that uncaught errors in the client code
154144
// are not intercepted by the Promise mechanism and therefore reported
@@ -211,32 +201,59 @@ class Chrome extends EventEmitter {
211201
}
212202
}
213203

214-
// establish the WebSocket connection and start processing user commands
215-
_connectToWebSocket() {
204+
_createStdioWrapper() {
205+
const stdio = new StdioWrapper(this.process.stdio[3], this.process.stdio[4]);
206+
this._close = stdio.close.bind(stdio);
207+
this._send = stdio.send.bind(stdio);
208+
return stdio;
209+
}
210+
211+
_createWebSocketWrapper() {
212+
if (this.secure) {
213+
this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, 'wss:');
214+
}
215+
const ws = new WebSocket(this.webSocketUrl);
216+
this._close = (callback) => {
217+
// don't close if it's already closed
218+
if (ws.readyState === 3) {
219+
callback();
220+
} else {
221+
// don't notify on user-initiated shutdown ('disconnect' event)
222+
ws.removeAllListeners('close');
223+
ws.once('close', () => {
224+
ws.removeAllListeners();
225+
callback();
226+
});
227+
ws.close();
228+
}
229+
};
230+
this._send = ws.send.bind(ws);
231+
return ws;
232+
}
233+
234+
// establish the connection wrapper and start processing user commands
235+
_connect() {
216236
return new Promise((fulfill, reject) => {
217-
// create the WebSocket
237+
let wrapper;
218238
try {
219-
if (this.secure) {
220-
this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, 'wss:');
221-
}
222-
this._ws = new WebSocket(this.webSocketUrl);
239+
wrapper = this.process ? this._createStdioWrapper() : this._createWebSocketWrapper();
223240
} catch (err) {
224-
// handles bad URLs
241+
// handle missing stdio streams, bad URLs...
225242
reject(err);
226243
return;
227244
}
228245
// set up event handlers
229-
this._ws.on('open', () => {
246+
wrapper.on('open', () => {
230247
fulfill();
231248
});
232-
this._ws.on('message', (data) => {
249+
wrapper.on('message', (data) => {
233250
const message = JSON.parse(data);
234251
this._handleMessage(message);
235252
});
236-
this._ws.on('close', (code) => {
253+
wrapper.on('close', (code) => {
237254
this.emit('disconnect');
238255
});
239-
this._ws.on('error', (err) => {
256+
wrapper.on('error', (err) => {
240257
reject(err);
241258
});
242259
});
@@ -278,7 +295,7 @@ class Chrome extends EventEmitter {
278295
id, method,
279296
params: params || {}
280297
};
281-
this._ws.send(JSON.stringify(message), (err) => {
298+
this._send(JSON.stringify(message), (err) => {
282299
if (err) {
283300
// handle low-level WebSocket errors
284301
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)