feat(appMode): support pipes for appMode (#2032)

This patch starts using pipes as a transport to
the browser instance instead of websocket.
This commit is contained in:
Andrey Lushnikov 2018-02-14 17:51:29 -08:00 committed by GitHub
parent 3656cc227f
commit e8a085ccfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 124 additions and 22 deletions

View File

@ -19,6 +19,7 @@ const debugSession = require('debug')('puppeteer:session');
const EventEmitter = require('events');
const WebSocket = require('ws');
const Pipe = require('./Pipe');
class Connection extends EventEmitter {
/**
@ -26,7 +27,7 @@ class Connection extends EventEmitter {
* @param {number=} delay
* @return {!Promise<!Connection>}
*/
static async create(url, delay = 0) {
static async createForWebSocket(url, delay = 0) {
return new Promise((resolve, reject) => {
const ws = new WebSocket(url, { perMessageDeflate: false });
ws.on('open', () => resolve(new Connection(url, ws, delay)));
@ -34,12 +35,22 @@ class Connection extends EventEmitter {
});
}
/**
* @param {!NodeJS.WritableStream} pipeWrite
* @param {!NodeJS.ReadableStream} pipeRead
* @param {number=} delay
* @return {!Connection}
*/
static createForPipe(pipeWrite, pipeRead, delay = 0) {
return new Connection('', new Pipe(pipeWrite, pipeRead), delay);
}
/**
* @param {string} url
* @param {!WebSocket} ws
* @param {!Puppeteer.ConnectionTransport} transport
* @param {number=} delay
*/
constructor(url, ws, delay = 0) {
constructor(url, transport, delay = 0) {
super();
this._url = url;
this._lastId = 0;
@ -47,9 +58,9 @@ class Connection extends EventEmitter {
this._callbacks = new Map();
this._delay = delay;
this._ws = ws;
this._ws.on('message', this._onMessage.bind(this));
this._ws.on('close', this._onClose.bind(this));
this._transport = transport;
this._transport.on('message', this._onMessage.bind(this));
this._transport.on('close', this._onClose.bind(this));
/** @type {!Map<string, !CDPSession>}*/
this._sessions = new Map();
}
@ -70,7 +81,7 @@ class Connection extends EventEmitter {
const id = ++this._lastId;
const message = JSON.stringify({id, method, params});
debugProtocol('SEND ► ' + message);
this._ws.send(message);
this._transport.send(message);
return new Promise((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
});
@ -120,7 +131,7 @@ class Connection extends EventEmitter {
this._closeCallback();
this._closeCallback = null;
}
this._ws.removeAllListeners();
this._transport.removeAllListeners();
for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear();
@ -131,7 +142,7 @@ class Connection extends EventEmitter {
dispose() {
this._onClose();
this._ws.close();
this._transport.close();
}
/**

View File

@ -43,7 +43,6 @@ const DEFAULT_ARGS = [
'--disable-translate',
'--metrics-recording-only',
'--no-first-run',
'--remote-debugging-port=0',
'--safebrowsing-disable-auto-update',
];
@ -51,6 +50,7 @@ const AUTOMATION_ARGS = [
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain',
'--remote-debugging-port=0',
];
class Launcher {
@ -60,15 +60,17 @@ class Launcher {
*/
static async launch(options) {
options = Object.assign({}, options || {});
console.assert(!options.ignoreDefaultArgs || !options.appMode, '`appMode` flag cannot be used together with `ignoreDefaultArgs`');
let temporaryUserDataDir = null;
const chromeArguments = [];
if (!options.ignoreDefaultArgs)
chromeArguments.push(...DEFAULT_ARGS);
if (options.appMode)
if (options.appMode) {
options.headless = false;
else if (!options.ignoreDefaultArgs)
chromeArguments.push('--remote-debugging-pipe');
} else if (!options.ignoreDefaultArgs) {
chromeArguments.push(...AUTOMATION_ARGS);
}
if (!options.args || !options.args.some(arg => arg.startsWith('--user-data-dir'))) {
if (!options.userDataDir)
@ -98,18 +100,19 @@ class Launcher {
if (Array.isArray(options.args))
chromeArguments.push(...options.args);
const stdio = options.dumpio ? ['inherit', 'inherit', 'inherit'] : ['pipe', 'pipe', 'pipe'];
if (options.appMode)
stdio.push('pipe', 'pipe');
const chromeProcess = childProcess.spawn(
chromeExecutable,
chromeArguments,
{
detached: true,
env: options.env || process.env
env: options.env || process.env,
stdio
}
);
if (options.dumpio) {
chromeProcess.stdout.pipe(process.stdout);
chromeProcess.stderr.pipe(process.stderr);
}
let chromeClosed = false;
const waitForChromeToClose = new Promise((fulfill, reject) => {
@ -136,10 +139,14 @@ class Launcher {
/** @type {?Connection} */
let connection = null;
try {
const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
const connectionDelay = options.slowMo || 0;
const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, timeout);
connection = await Connection.create(browserWSEndpoint, connectionDelay);
if (!options.appMode) {
const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, timeout);
connection = await Connection.createForWebSocket(browserWSEndpoint, connectionDelay);
} else {
connection = Connection.createForPipe(/** @type {!NodeJS.WritableStream} */(chromeProcess.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (chromeProcess.stdio[4]), connectionDelay);
}
return Browser.create(connection, options, chromeProcess, killChrome);
} catch (e) {
forceKillChrome();
@ -198,7 +205,7 @@ class Launcher {
*/
static async connect(options = {}) {
const connectionDelay = options.slowMo || 0;
const connection = await Connection.create(options.browserWSEndpoint, connectionDelay);
const connection = await Connection.createForWebSocket(options.browserWSEndpoint, connectionDelay);
return Browser.create(connection, options, null, () => connection.send('Browser.close'));
}
}

69
lib/Pipe.js Normal file
View File

@ -0,0 +1,69 @@
/**
* 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 {helper} = require('./helper');
const EventEmitter = require('events');
class Pipe extends EventEmitter {
/**
* @param {!NodeJS.WritableStream} pipeWrite
* @param {!NodeJS.ReadableStream} pipeRead
*/
constructor(pipeWrite, pipeRead) {
super();
this._pipeWrite = pipeWrite;
this._pendingMessage = '';
this._eventListeners = [
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer))
];
}
/**
* @param {string} message
*/
send(message) {
this._pipeWrite.write(message);
this._pipeWrite.write('\n');
}
/**
* @param {!Buffer} buffer
*/
_dispatch(buffer) {
let end = buffer.indexOf('\n');
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('\n', start);
while (end !== -1) {
this.emit('message', buffer.toString(undefined, start, end));
start = end + 1;
end = buffer.indexOf('\n', start);
}
this._pendingMessage = buffer.toString(undefined, start);
}
close() {
this._pipeWrite = null;
helper.removeEventListeners(this._eventListeners);
}
}
module.exports = Pipe;

4
lib/externs.d.ts vendored
View File

@ -25,5 +25,9 @@ export class JSHandle extends RealJSHandle {}
export class ExecutionContext extends RealExecutionContext {}
export class Page extends RealPage {}
export interface ConnectionTransport extends NodeJS.EventEmitter {
send(string);
close();
}
export interface ChildProcess extends child_process.ChildProcess {}

View File

@ -141,6 +141,16 @@ describe('Puppeteer', function() {
rm(downloadsFolder);
});
});
describe('AppMode', function() {
it('should work', async() => {
const options = Object.assign({appMode: true}, defaultBrowserOptions);
const browser = await puppeteer.launch(options);
const page = await browser.newPage();
expect(await page.evaluate('11 * 11')).toBe(121);
await page.close();
await browser.close();
});
});
describe('Puppeteer.launch', function() {
it('should support ignoreHTTPSErrors option', async({httpsServer}) => {
const options = Object.assign({ignoreHTTPSErrors: true}, defaultBrowserOptions);

View File

@ -30,6 +30,7 @@ const EXCLUDE_CLASSES = new Set([
'Multimap',
'NavigatorWatcher',
'NetworkManager',
'Pipe',
'TaskQueue',
'WaitTask',
]);