Skip to content

Commit

Permalink
feat: support for --remote-debugging-pipe transport
Browse files Browse the repository at this point in the history
  • Loading branch information
flotwig committed Dec 29, 2020
1 parent b354aa1 commit baad1bd
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 43 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,12 @@ Connects to a remote instance using the [Chrome Debugging Protocol].
- `protocol`: [Chrome Debugging Protocol] descriptor object. Defaults to use the
protocol chosen according to the `local` option;
- `local`: a boolean indicating whether the protocol must be fetched *remotely*
or if the local version must be used. It has no effect if the `protocol`
option is set. Defaults to `false`.
or if the local version must be used. It has no effect if the `protocol` or
`process` option is set. Defaults to `false`.
- `process`: a `ChildProcess` object that represents a Chrome instance launched
with `--remote-debugging-pipe`. If passed, websocket-related options will be
ignored and communications will occur over stdio instead. Note: the `protocol`
cannot be fetched remotely if a `process` is passed.

These options are also valid properties of all the instances of the `CDP`
class. In addition to that, the `webSocketUrl` field contains the currently used
Expand Down
99 changes: 58 additions & 41 deletions lib/chrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const WebSocket = require('ws');
const api = require('./api.js');
const defaults = require('./defaults.js');
const devtools = require('./devtools.js');
const StdioWrapper = require('./stdio-wrapper.js');

class ProtocolError extends Error {
constructor(request, response) {
Expand Down Expand Up @@ -55,8 +56,9 @@ class Chrome extends EventEmitter {
this.useHostName = !!(options.useHostName);
this.alterPath = options.alterPath || ((path) => path);
this.protocol = options.protocol;
this.local = !!(options.local);
this.local = !!(options.local || options.process);
this.target = options.target || defaultTarget;
this.process = options.process;
// locals
this._notifier = notifier;
this._callbacks = {};
Expand Down Expand Up @@ -101,26 +103,12 @@ class Chrome extends EventEmitter {
}

close(callback) {
const closeWebSocket = (callback) => {
// don't close if it's already closed
if (this._ws.readyState === 3) {
callback();
} else {
// don't notify on user-initiated shutdown ('disconnect' event)
this._ws.removeAllListeners('close');
this._ws.once('close', () => {
this._ws.removeAllListeners();
callback();
});
this._ws.close();
}
};
if (typeof callback === 'function') {
closeWebSocket(callback);
this._close(callback);
return undefined;
} else {
return new Promise((fulfill, reject) => {
closeWebSocket(fulfill);
this._close(fulfill);
});
}
}
Expand All @@ -135,20 +123,22 @@ class Chrome extends EventEmitter {
alterPath: this.alterPath
};
try {
// fetch the WebSocket debugger URL
const url = await this._fetchDebuggerURL(options);
// allow the user to alter the URL
const urlObject = parseUrl(url);
urlObject.pathname = options.alterPath(urlObject.pathname);
this.webSocketUrl = formatUrl(urlObject);
// update the connection parameters using the debugging URL
options.host = urlObject.hostname;
options.port = urlObject.port || options.port;
if (!this.process) {
// fetch the WebSocket debugger URL
const url = await this._fetchDebuggerURL(options);
// allow the user to alter the URL
const urlObject = parseUrl(url);
urlObject.pathname = options.alterPath(urlObject.pathname);
this.webSocketUrl = formatUrl(urlObject);
// update the connection parameters using the debugging URL
options.host = urlObject.hostname;
options.port = urlObject.port || options.port;
}
// fetch the protocol and prepare the API
const protocol = await this._fetchProtocol(options);
api.prepare(this, protocol);
// finally connect to the WebSocket
await this._connectToWebSocket();
// finally connect to the WebSocket or stdio
await this._connect();
// since the handler is executed synchronously, the emit() must be
// performed in the next tick so that uncaught errors in the client code
// are not intercepted by the Promise mechanism and therefore reported
Expand Down Expand Up @@ -211,32 +201,59 @@ class Chrome extends EventEmitter {
}
}

// establish the WebSocket connection and start processing user commands
_connectToWebSocket() {
_createStdioWrapper() {
const stdio = new StdioWrapper(this.process.stdio[3], this.process.stdio[4]);
this._close = stdio.close.bind(stdio);
this._send = stdio.send.bind(stdio);
return stdio;
}

_createWebSocketWrapper() {
if (this.secure) {
this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, 'wss:');
}
const ws = new WebSocket(this.webSocketUrl);
this._close = (callback) => {
// don't close if it's already closed
if (ws.readyState === 3) {
callback();
} else {
// don't notify on user-initiated shutdown ('disconnect' event)
ws.removeAllListeners('close');
ws.once('close', () => {
ws.removeAllListeners();
callback();
});
ws.close();
}
};
this._send = ws.send.bind(ws);
return ws;
}

// establish the connection wrapper and start processing user commands
_connect() {
return new Promise((fulfill, reject) => {
// create the WebSocket
let wrapper;
try {
if (this.secure) {
this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, 'wss:');
}
this._ws = new WebSocket(this.webSocketUrl);
wrapper = this.process ? this._createStdioWrapper() : this._createWebSocketWrapper();
} catch (err) {
// handles bad URLs
// handle missing stdio streams, bad URLs...
reject(err);
return;
}
// set up event handlers
this._ws.on('open', () => {
wrapper.on('open', () => {
fulfill();
});
this._ws.on('message', (data) => {
wrapper.on('message', (data) => {
const message = JSON.parse(data);
this._handleMessage(message);
});
this._ws.on('close', (code) => {
wrapper.on('close', (code) => {
this.emit('disconnect');
});
this._ws.on('error', (err) => {
wrapper.on('error', (err) => {
reject(err);
});
});
Expand Down Expand Up @@ -278,7 +295,7 @@ class Chrome extends EventEmitter {
id, method,
params: params || {}
};
this._ws.send(JSON.stringify(message), (err) => {
this._send(JSON.stringify(message), (err) => {
if (err) {
// handle low-level WebSocket errors
if (typeof callback === 'function') {
Expand Down
87 changes: 87 additions & 0 deletions lib/stdio-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';

// Adapted from https://github.com/puppeteer/puppeteer/blob/7a2a41f2087b07e8ef1feaf3881bdcc3fd4922ca/src/PipeTransport.js

/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/

const { EventEmitter } = require('events');

function addEventListener(emitter, eventName, handler) {
emitter.on(eventName, handler);
return { emitter, eventName, handler };
}

function removeEventListeners(listeners) {
for (const listener of listeners)
listener.emitter.removeListener(listener.eventName, listener.handler);
listeners.length = 0;
}

// wrapper for null-terminated stdio message transport
class StdioWrapper extends EventEmitter {
constructor(pipeWrite, pipeRead) {
super();
this._pipeWrite = pipeWrite;
this._pendingMessage = '';
this._eventListeners = [
addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
addEventListener(pipeRead, 'close', () => this.emit('close')),
addEventListener(pipeRead, 'error', (err) => this.emit('error', err)),
addEventListener(pipeWrite, 'error', (err) => this.emit('error', err)),
];
process.nextTick(() => {
this.emit('open');
});
}

send(message, callback) {
try {
this._pipeWrite.write(message);
this._pipeWrite.write('\0');
callback();
} catch (err) {
callback(err);
}
}

_dispatch(buffer) {
let end = buffer.indexOf('\0');
if (end === -1) {
this._pendingMessage += buffer.toString();
return;
}
const message = this._pendingMessage + buffer.toString(undefined, 0, end);

this.emit('message', message);

let start = end + 1;
end = buffer.indexOf('\0', start);
while (end !== -1) {
this.emit('message', buffer.toString(undefined, start, end));
start = end + 1;
end = buffer.indexOf('\0', start);
}
this._pendingMessage = buffer.toString(undefined, start);
}

close() {
this._pipeWrite = null;
removeEventListeners(this._eventListeners);
}
}

module.exports = StdioWrapper;

0 comments on commit baad1bd

Please sign in to comment.