Roll chromium to 492629 (#230)

This patch
- rolls chromium to 492629
- migrates connection establishing to use browser target. This migration means
  that now we have a single websocket connection to browser (implemented
  in Connection class). A connection to a particular target is
  incapsulated in a new Session class.
This commit is contained in:
Andrey Lushnikov 2017-08-09 16:14:00 -07:00 committed by GitHub
parent 29adc5dc80
commit fe06c896eb
14 changed files with 162 additions and 42 deletions

View File

@ -92,9 +92,10 @@ class Browser {
await this._ensureChromeIsRunning(); await this._ensureChromeIsRunning();
if (!this._chromeProcess || this._terminated) if (!this._chromeProcess || this._terminated)
throw new Error('ERROR: this chrome instance is not alive any more!'); throw new Error('ERROR: this chrome instance is not alive any more!');
let client = await Connection.create(this._remoteDebuggingPort, this._connectionDelay);
let page = await Page.create(client, this._ignoreHTTPSErrors, this._screenshotTaskQueue); const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank'});
return page; const client = await this._connection.createSession(targetId);
return await Page.create(client, this._ignoreHTTPSErrors, this._screenshotTaskQueue);
} }
/** /**
@ -133,15 +134,17 @@ class Browser {
this._chromeProcess.stderr.pipe(this.stderr); this._chromeProcess.stderr.pipe(this.stderr);
this._chromeProcess.stdout.pipe(this.stdout); this._chromeProcess.stdout.pipe(this.stdout);
this._remoteDebuggingPort = await waitForRemoteDebuggingPort(this._chromeProcess); let {port, browserTargetId} = await waitForRemoteDebuggingPort(this._chromeProcess);
// Failed to connect to browser. // Failed to connect to browser.
if (this._remoteDebuggingPort === -1) { if (port === -1) {
this._chromeProcess.kill(); this._chromeProcess.kill();
throw new Error('Failed to connect to chrome!'); throw new Error('Failed to connect to chrome!');
} }
if (this._terminated) if (this._terminated)
throw new Error('Failed to launch chrome! ' + stderr); throw new Error('Failed to launch chrome! ' + stderr);
this._remoteDebuggingPort = port;
this._connection = await Connection.create(port, browserTargetId, this._connectionDelay);
} }
close() { close() {
@ -168,11 +171,11 @@ function waitForRemoteDebuggingPort(chromeProcess) {
* @param {string} line * @param {string} line
*/ */
function onLine(line) { function onLine(line) {
const match = line.match(/^DevTools listening on .*:(\d+)$/); const match = line.match(/^DevTools listening on .*:(\d+)(\/.*)$/);
if (!match) if (!match)
return; return;
rl.removeListener('line', onLine); rl.removeListener('line', onLine);
fulfill(Number.parseInt(match[1], 10)); fulfill({port: Number.parseInt(match[1], 10), browserTargetId: match[2]});
} }
}); });
} }

View File

@ -23,14 +23,11 @@ const COMMAND_TIMEOUT = 10000;
class Connection extends EventEmitter { class Connection extends EventEmitter {
/** /**
* @param {number} port * @param {number} port
* @param {string} targetId
* @param {!WebSocket} ws * @param {!WebSocket} ws
* @param {number=} delay * @param {number=} delay
*/ */
constructor(port, targetId, ws, delay) { constructor(ws, delay) {
super(); super();
this._port = port;
this._targetId = targetId;
this._lastId = 0; this._lastId = 0;
/** @type {!Map<number, {resolve: function, reject: function, method: string}>}*/ /** @type {!Map<number, {resolve: function, reject: function, method: string}>}*/
this._callbacks = new Map(); this._callbacks = new Map();
@ -39,13 +36,8 @@ class Connection extends EventEmitter {
this._ws = ws; this._ws = ws;
this._ws.on('message', this._onMessage.bind(this)); this._ws.on('message', this._onMessage.bind(this));
this._ws.on('close', this._onClose.bind(this)); this._ws.on('close', this._onClose.bind(this));
} /** @type {!Map<string, !Session>}*/
this._sessions = new Map();
/**
* @return {string}
*/
targetId() {
return this._targetId;
} }
/** /**
@ -80,22 +72,47 @@ class Connection extends EventEmitter {
callback.resolve(object.result); callback.resolve(object.result);
} else { } else {
console.assert(!object.id); console.assert(!object.id);
if (object.method === 'Target.receivedMessageFromTarget') {
const session = this._sessions.get(object.params.sessionId);
if (session)
session._onMessage(object.params.message);
} else if (object.method === 'Target.detachedFromTarget') {
const session = this._sessions.get(object.params.sessionId);
if (session)
session._onClosed();
this._sessions.delete(object.params.sessionId);
} else {
this.emit(object.method, object.params); this.emit(object.method, object.params);
} }
} }
}
_onClose() { _onClose() {
this._ws.removeAllListeners(); this._ws.removeAllListeners();
this._ws.close();
for (let callback of this._callbacks.values()) for (let callback of this._callbacks.values())
callback.reject(new Error(`Protocol error (${callback.method}): Target closed.`)); callback.reject(new Error(`Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear();
for (let session of this._sessions.values())
session._onClosed();
this._sessions.clear();
} }
/** /**
* @return {!Promise} * @return {!Promise}
*/ */
async dispose() { async dispose() {
await runJsonCommand(this._port, `close/${this._targetId}`); this._ws.close();
}
/**
* @param {string} targetId
* @return {!Promise<!Session>}
*/
async createSession(targetId) {
const {sessionId} = await this.send('Target.attachToTarget', {targetId});
const session = new Session(this, targetId, sessionId);
this._sessions.set(sessionId, session);
return session;
} }
/** /**
@ -103,13 +120,11 @@ class Connection extends EventEmitter {
* @param {number=} delay * @param {number=} delay
* @return {!Promise<!Connection>} * @return {!Promise<!Connection>}
*/ */
static async create(port, delay) { static async create(port, targetId, delay) {
let newTab = await runJsonCommand(port, 'new'); const url = `ws://localhost:${port}${targetId}`;
let url = newTab.webSocketDebuggerUrl;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let ws = new WebSocket(url, { perMessageDeflate: false }); let ws = new WebSocket(url, { perMessageDeflate: false });
ws.on('open', () => resolve(new Connection(port, newTab.id, ws, delay))); ws.on('open', () => resolve(new Connection(ws, delay)));
ws.on('error', reject); ws.on('error', reject);
}); });
} }
@ -123,6 +138,82 @@ class Connection extends EventEmitter {
} }
} }
class Session extends EventEmitter {
/**
* @param {!Connection} connection
* @param {string} targetId
* @param {string} sessionId
*/
constructor(connection, targetId, sessionId) {
super();
this._lastId = 0;
/** @type {!Map<number, {resolve: function, reject: function, method: string}>}*/
this._callbacks = new Map();
this._connection = connection;
this._targetId = targetId;
this._sessionId = sessionId;
}
/**
* @return {string}
*/
targetId() {
return this._targetId;
}
/**
* @param {string} method
* @param {!Object=} params
* @return {!Promise<?Object>}
*/
async send(method, params = {}) {
let id = ++this._lastId;
let message = JSON.stringify({id, method, params});
this._connection.send('Target.sendMessageToTarget', {sessionId: this._sessionId, message}).catch(e => {
// The response from target might have been already dispatched.
if (!this._callbacks.has(id))
return;
let callback = this._callbacks.get(id);
this._callbacks.delete(object.id);
callback.reject(e);
});
return new Promise((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, method});
});
}
/**
* @param {string} message
*/
_onMessage(message) {
let object = JSON.parse(message);
if (object.id && this._callbacks.has(object.id)) {
let 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} ${object.error.data}`));
else
callback.resolve(object.result);
} else {
console.assert(!object.id);
this.emit(object.method, object.params);
}
}
/**
* @return {!Promise}
*/
async dispose() {
await this._connection.send('Target.closeTarget', {targetId: this._targetId});
}
_onClosed() {
for (let callback of this._callbacks.values())
callback.reject(new Error(`Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear();
}
}
/** /**
* @param {number} port * @param {number} port
* @param {string} command * @param {string} command

View File

@ -18,7 +18,7 @@ const helper = require('./helper');
class Dialog { class Dialog {
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {!Dialog.Type} type * @param {!Dialog.Type} type
* @param {string} message * @param {string} message
*/ */

View File

@ -16,7 +16,7 @@
class EmulationManager { class EmulationManager {
/** /**
* @param {!Connection} client * @param {!Session} client
*/ */
constructor(client) { constructor(client) {
this._client = client; this._client = client;

View File

@ -21,7 +21,7 @@ const helper = require('./helper');
class FrameManager extends EventEmitter { class FrameManager extends EventEmitter {
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {!Object} frameTree * @param {!Object} frameTree
* @param {!Mouse} mouse * @param {!Mouse} mouse
*/ */
@ -144,7 +144,7 @@ FrameManager.Events = {
*/ */
class Frame { class Frame {
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {!Mouse} mouse * @param {!Mouse} mouse
* @param {?Frame} parentFrame * @param {?Frame} parentFrame
* @param {string} frameId * @param {string} frameId

View File

@ -18,7 +18,7 @@ const helper = require('./helper');
class Keyboard { class Keyboard {
/** /**
* @param {!Connection} client * @param {!Session} client
*/ */
constructor(client) { constructor(client) {
this._client = client; this._client = client;
@ -95,7 +95,7 @@ class Keyboard {
class Mouse { class Mouse {
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {!Keyboard} keyboard * @param {!Keyboard} keyboard
*/ */
constructor(client, keyboard) { constructor(client, keyboard) {

View File

@ -18,7 +18,7 @@ const helper = require('./helper');
class NavigatorWatcher { class NavigatorWatcher {
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {boolean} ignoreHTTPSErrors * @param {boolean} ignoreHTTPSErrors
* @param {!Object=} options * @param {!Object=} options
*/ */

View File

@ -18,7 +18,7 @@ const helper = require('./helper');
class NetworkManager extends EventEmitter { class NetworkManager extends EventEmitter {
/** /**
* @param {!Connection} client * @param {!Session} client
*/ */
constructor(client) { constructor(client) {
super(); super();
@ -165,7 +165,7 @@ helper.tracePublicAPI(Request);
class Response { class Response {
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {?Request} request * @param {?Request} request
* @param {!Object} payload * @param {!Object} payload
*/ */
@ -223,7 +223,7 @@ helper.tracePublicAPI(Response);
class InterceptedRequest { class InterceptedRequest {
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {string} interceptionId * @param {string} interceptionId
* @param {!Object} payload * @param {!Object} payload
*/ */

View File

@ -28,7 +28,8 @@ const helper = require('./helper');
class Page extends EventEmitter { class Page extends EventEmitter {
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {string} sessionId
* @param {boolean} ignoreHTTPSErrors * @param {boolean} ignoreHTTPSErrors
* @param {!TaskQueue} screenshotTaskQueue * @param {!TaskQueue} screenshotTaskQueue
* @return {!Promise<!Page>} * @return {!Promise<!Page>}
@ -50,7 +51,7 @@ class Page extends EventEmitter {
} }
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {boolean} ignoreHTTPSErrors * @param {boolean} ignoreHTTPSErrors
* @param {!TaskQueue} screenshotTaskQueue * @param {!TaskQueue} screenshotTaskQueue
*/ */

View File

@ -18,7 +18,7 @@ const helper = require('./helper');
class Tracing { class Tracing {
/** /**
* @param {!Connection} client * @param {!Session} client
*/ */
constructor(client) { constructor(client) {
this._client = client; this._client = client;

View File

@ -49,7 +49,7 @@ class Helper {
} }
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {!Object} remoteObject * @param {!Object} remoteObject
* @return {!Promise<!Object>} * @return {!Promise<!Object>}
*/ */
@ -96,7 +96,7 @@ class Helper {
} }
/** /**
* @param {!Connection} client * @param {!Session} client
* @param {!Object} remoteObject * @param {!Object} remoteObject
* @return {!Promise} * @return {!Promise}
*/ */

View File

@ -29,7 +29,7 @@
"ws": "^3.0.0" "ws": "^3.0.0"
}, },
"puppeteer": { "puppeteer": {
"chromium_revision": "491334" "chromium_revision": "492629"
}, },
"devDependencies": { "devDependencies": {
"commonmark": "^0.27.0", "commonmark": "^0.27.0",

View File

@ -95,6 +95,15 @@ describe('Browser', function() {
expect(response.ok).toBe(true); expect(response.ok).toBe(true);
browser.close(); browser.close();
})); }));
it('should reject all promises when browser is closed', SX(async function() {
let browser = new Browser(defaultBrowserOptions);
let page = await browser.newPage();
let error = null;
let neverResolves = page.evaluate(() => new Promise(r => {})).catch(e => error = e);
browser.close();
await neverResolves;
expect(error.message).toContain('Protocol error');
}));
}); });
describe('Page', function() { describe('Page', function() {
@ -131,6 +140,21 @@ describe('Page', function() {
})); }));
}); });
describe('Page.close', function() {
it('should reject all promises when page is closed', SX(async function() {
let newPage = await browser.newPage();
let neverResolves = newPage.evaluate(() => new Promise(r => {}));
newPage.close();
let error = null;
try {
await neverResolves;
} catch (e) {
error = e;
}
expect(error.message).toContain('Protocol error');
}));
});
describe('Page.evaluate', function() { describe('Page.evaluate', function() {
it('should work', SX(async function() { it('should work', SX(async function() {
let result = await page.evaluate(() => 7 * 3); let result = await page.evaluate(() => 7 * 3);

View File

@ -27,6 +27,7 @@ const EXCLUDE_CLASSES = new Set([
'NavigatorWatcher', 'NavigatorWatcher',
'NetworkManager', 'NetworkManager',
'ProxyStream', 'ProxyStream',
'Session',
'TaskQueue', 'TaskQueue',
'WaitTask', 'WaitTask',
]); ]);