From 67932b87c1ce12aa3eb3a473b73cb4eb4c803b03 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Wed, 14 Jun 2017 15:45:59 -0700 Subject: [PATCH] Drop the chrome-remote-interface dependency This patch drops the chrome-remote-interface dependency and introduces Connection class which handles all the communication with remote target. Closes #3 --- lib/Browser.js | 14 ++--- lib/Connection.js | 152 ++++++++++++++++++++++++++++++++++++++++++++++ lib/Page.js | 2 +- package.json | 1 - 4 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 lib/Connection.js diff --git a/lib/Browser.js b/lib/Browser.js index b2166ff3532..46ae287474a 100644 --- a/lib/Browser.js +++ b/lib/Browser.js @@ -14,13 +14,13 @@ * limitations under the License. */ -var CDP = require('chrome-remote-interface'); var http = require('http'); var path = require('path'); var removeRecursive = require('rimraf').sync; var Page = require('./Page'); var childProcess = require('child_process'); var Downloader = require('../utils/ChromiumDownloader'); +var Connection = require('./Connection'); var CHROME_PROFILE_PATH = path.resolve(__dirname, '..', '.dev_profile'); var browserId = 0; @@ -63,7 +63,6 @@ class Browser { this._chromeArguments.push(...options.args); this._terminated = false; this._chromeProcess = null; - this._tabSymbol = Symbol('Browser.TabSymbol'); } /** @@ -73,10 +72,8 @@ class Browser { await this._ensureChromeIsRunning(); if (!this._chromeProcess || this._terminated) throw new Error('ERROR: this chrome instance is not alive any more!'); - var tab = await CDP.New({port: this._remoteDebuggingPort}); - var client = await CDP({tab: tab, port: this._remoteDebuggingPort}); + var client = await Connection.create(this._remoteDebuggingPort); var page = await Page.create(this, client); - page[this._tabSymbol] = tab; return page; } @@ -86,10 +83,7 @@ class Browser { async closePage(page) { if (!this._chromeProcess || this._terminated) throw new Error('ERROR: this chrome instance is not running'); - var tab = page[this._tabSymbol]; - if (!tab) - throw new Error('ERROR: page does not belong to this chrome instance'); - await CDP.Close({id: tab.id, port: this._remoteDebuggingPort}); + await page.close(); } /** @@ -97,7 +91,7 @@ class Browser { */ async version() { await this._ensureChromeIsRunning(); - var version = await CDP.Version({port: this._remoteDebuggingPort}); + var version = await Connection.version(this._remoteDebuggingPort); return version.Browser; } diff --git a/lib/Connection.js b/lib/Connection.js new file mode 100644 index 00000000000..cc1735a131e --- /dev/null +++ b/lib/Connection.js @@ -0,0 +1,152 @@ +/** + * Copyright 2017 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. + */ + +var EventEmitter = require('events'); +var WebSocket = require('ws'); +var http = require('http'); +const COMMAND_TIMEOUT = 10000; + +class Connection extends EventEmitter { + /** + * @param {number} port + * @param {string} pageId + * @param {!WebSocket} ws + */ + constructor(port, pageId, ws) { + super(); + this._port = port; + this._pageId = pageId; + this._lastId = 0; + /** @type {!Map}*/ + this._callbacks = new Map(); + + this._ws = ws; + this._ws.on('message', this._onMessage.bind(this)); + this._ws.on('close', this._onClose.bind(this)); + } + + /** + * @param {string} method + * @param {(!Object|undefined)} params + * @return {!Promise} + */ + send(method, params = {}) { + var id = ++this._lastId; + var message = JSON.stringify({id, method, params}); + this._ws.send(message); + return new Promise((resolve, reject) => { + this._callbacks.set(id, {resolve, reject, method}); + }); + } + + /** + * @param {string} message + */ + _onMessage(message) { + var object = JSON.parse(message); + if (object.id && this._callbacks.has(object.id)) { + var callback = this._callbacks.get(object.id); + this._callbacks.delete(object.id); + if (object.error) + callback.reject(new Error(`Protocol error (${callback.method}): ${object.error.message}`)); + else + callback.resolve(object.result); + } else { + console.assert(!object.id); + this.emit(object.method, object.params); + } + } + + _onClose() { + this._ws.removeAllListeners(); + this._ws.close(); + } + + /** + * @return {!Promise} + */ + async dispose() { + await runJsonCommand(this._port, `close/${this._pageId}`); + } + + /** + * @param {number} port + * @return {!Promise} + */ + static async create(port) { + var newTab = await runJsonCommand(port, 'new'); + var url = newTab.webSocketDebuggerUrl; + + return new Promise((resolve, reject) => { + var ws = new WebSocket(url, { perMessageDeflate: false }); + ws.on('open', () => resolve(new Connection(port, newTab.id, ws))); + ws.on('error', reject); + }); + } + + /** + * @param {number} port + * @return {!Promise} + */ + static version(port) { + return runJsonCommand(port, 'version'); + } +} + +/** + * @param {number} port + * @param {string} command + * @return {!Promise} + */ +function runJsonCommand(port, command) { + var request = http.get({ + hostname: 'localhost', + port: port, + path: '/json/' + command + }, onResponse); + request.setTimeout(COMMAND_TIMEOUT, onTimeout); + var resolve, reject; + return new Promise((res, rej) => { resolve = res; reject = rej; }); + + function onResponse(response) { + var data = ''; + response.on('data', chunk => data += chunk); + response.on('end', () => { + if (response.statusCode !== 200) { + reject(new Error(`Protocol JSON API error (${command}), status: ${response.statusCode}`)); + return; + } + // In the case of 'close' & 'activate' Chromium returns a string rather than JSON: goo.gl/7v27xD + if (data === 'Target is closing' || data === 'Target activated') { + resolve({message: data}); + return; + } + try { + resolve(JSON.parse(data)); + } catch (e) { + reject(e); + } + }); + } + + function onTimeout() { + request.abort(); + // Reject on error with code specifically indicating timeout in connection setup. + reject(new Error('Timeout waiting for initial Debugger Protocol connection.')); + } +} + +module.exports = Connection; diff --git a/lib/Page.js b/lib/Page.js index e85673ca4ac..096d49a26a1 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -474,7 +474,7 @@ class Page extends EventEmitter { * @return {!Promise} */ async close() { - return this._browser.closePage(this); + await this._client.dispose(); } } diff --git a/package.json b/package.json index 67185285099..415089cc5f0 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "author": "The Chromium Authors", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "chrome-remote-interface": "^0.18.0", "extract-zip": "^1.6.5", "mime": "^1.3.4", "progress": "^2.0.0",