mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
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:
parent
3656cc227f
commit
e8a085ccfb
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
if (!options.appMode) {
|
||||
const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
|
||||
const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, timeout);
|
||||
connection = await Connection.create(browserWSEndpoint, connectionDelay);
|
||||
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
69
lib/Pipe.js
Normal 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
4
lib/externs.d.ts
vendored
@ -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 {}
|
||||
|
10
test/test.js
10
test/test.js
@ -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);
|
||||
|
@ -30,6 +30,7 @@ const EXCLUDE_CLASSES = new Set([
|
||||
'Multimap',
|
||||
'NavigatorWatcher',
|
||||
'NetworkManager',
|
||||
'Pipe',
|
||||
'TaskQueue',
|
||||
'WaitTask',
|
||||
]);
|
||||
|
Loading…
Reference in New Issue
Block a user