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 EventEmitter = require('events');
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
|
const Pipe = require('./Pipe');
|
||||||
|
|
||||||
class Connection extends EventEmitter {
|
class Connection extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
@ -26,7 +27,7 @@ class Connection extends EventEmitter {
|
|||||||
* @param {number=} delay
|
* @param {number=} delay
|
||||||
* @return {!Promise<!Connection>}
|
* @return {!Promise<!Connection>}
|
||||||
*/
|
*/
|
||||||
static async create(url, delay = 0) {
|
static async createForWebSocket(url, delay = 0) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ws = new WebSocket(url, { perMessageDeflate: false });
|
const ws = new WebSocket(url, { perMessageDeflate: false });
|
||||||
ws.on('open', () => resolve(new Connection(url, ws, delay)));
|
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 {string} url
|
||||||
* @param {!WebSocket} ws
|
* @param {!Puppeteer.ConnectionTransport} transport
|
||||||
* @param {number=} delay
|
* @param {number=} delay
|
||||||
*/
|
*/
|
||||||
constructor(url, ws, delay = 0) {
|
constructor(url, transport, delay = 0) {
|
||||||
super();
|
super();
|
||||||
this._url = url;
|
this._url = url;
|
||||||
this._lastId = 0;
|
this._lastId = 0;
|
||||||
@ -47,9 +58,9 @@ class Connection extends EventEmitter {
|
|||||||
this._callbacks = new Map();
|
this._callbacks = new Map();
|
||||||
this._delay = delay;
|
this._delay = delay;
|
||||||
|
|
||||||
this._ws = ws;
|
this._transport = transport;
|
||||||
this._ws.on('message', this._onMessage.bind(this));
|
this._transport.on('message', this._onMessage.bind(this));
|
||||||
this._ws.on('close', this._onClose.bind(this));
|
this._transport.on('close', this._onClose.bind(this));
|
||||||
/** @type {!Map<string, !CDPSession>}*/
|
/** @type {!Map<string, !CDPSession>}*/
|
||||||
this._sessions = new Map();
|
this._sessions = new Map();
|
||||||
}
|
}
|
||||||
@ -70,7 +81,7 @@ class Connection extends EventEmitter {
|
|||||||
const id = ++this._lastId;
|
const id = ++this._lastId;
|
||||||
const message = JSON.stringify({id, method, params});
|
const message = JSON.stringify({id, method, params});
|
||||||
debugProtocol('SEND ► ' + message);
|
debugProtocol('SEND ► ' + message);
|
||||||
this._ws.send(message);
|
this._transport.send(message);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
||||||
});
|
});
|
||||||
@ -120,7 +131,7 @@ class Connection extends EventEmitter {
|
|||||||
this._closeCallback();
|
this._closeCallback();
|
||||||
this._closeCallback = null;
|
this._closeCallback = null;
|
||||||
}
|
}
|
||||||
this._ws.removeAllListeners();
|
this._transport.removeAllListeners();
|
||||||
for (const callback of this._callbacks.values())
|
for (const callback of this._callbacks.values())
|
||||||
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
|
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
|
||||||
this._callbacks.clear();
|
this._callbacks.clear();
|
||||||
@ -131,7 +142,7 @@ class Connection extends EventEmitter {
|
|||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this._onClose();
|
this._onClose();
|
||||||
this._ws.close();
|
this._transport.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,7 +43,6 @@ const DEFAULT_ARGS = [
|
|||||||
'--disable-translate',
|
'--disable-translate',
|
||||||
'--metrics-recording-only',
|
'--metrics-recording-only',
|
||||||
'--no-first-run',
|
'--no-first-run',
|
||||||
'--remote-debugging-port=0',
|
|
||||||
'--safebrowsing-disable-auto-update',
|
'--safebrowsing-disable-auto-update',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -51,6 +50,7 @@ const AUTOMATION_ARGS = [
|
|||||||
'--enable-automation',
|
'--enable-automation',
|
||||||
'--password-store=basic',
|
'--password-store=basic',
|
||||||
'--use-mock-keychain',
|
'--use-mock-keychain',
|
||||||
|
'--remote-debugging-port=0',
|
||||||
];
|
];
|
||||||
|
|
||||||
class Launcher {
|
class Launcher {
|
||||||
@ -60,15 +60,17 @@ class Launcher {
|
|||||||
*/
|
*/
|
||||||
static async launch(options) {
|
static async launch(options) {
|
||||||
options = Object.assign({}, options || {});
|
options = Object.assign({}, options || {});
|
||||||
|
console.assert(!options.ignoreDefaultArgs || !options.appMode, '`appMode` flag cannot be used together with `ignoreDefaultArgs`');
|
||||||
let temporaryUserDataDir = null;
|
let temporaryUserDataDir = null;
|
||||||
const chromeArguments = [];
|
const chromeArguments = [];
|
||||||
if (!options.ignoreDefaultArgs)
|
if (!options.ignoreDefaultArgs)
|
||||||
chromeArguments.push(...DEFAULT_ARGS);
|
chromeArguments.push(...DEFAULT_ARGS);
|
||||||
|
if (options.appMode) {
|
||||||
if (options.appMode)
|
|
||||||
options.headless = false;
|
options.headless = false;
|
||||||
else if (!options.ignoreDefaultArgs)
|
chromeArguments.push('--remote-debugging-pipe');
|
||||||
|
} else if (!options.ignoreDefaultArgs) {
|
||||||
chromeArguments.push(...AUTOMATION_ARGS);
|
chromeArguments.push(...AUTOMATION_ARGS);
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.args || !options.args.some(arg => arg.startsWith('--user-data-dir'))) {
|
if (!options.args || !options.args.some(arg => arg.startsWith('--user-data-dir'))) {
|
||||||
if (!options.userDataDir)
|
if (!options.userDataDir)
|
||||||
@ -98,18 +100,19 @@ class Launcher {
|
|||||||
if (Array.isArray(options.args))
|
if (Array.isArray(options.args))
|
||||||
chromeArguments.push(...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(
|
const chromeProcess = childProcess.spawn(
|
||||||
chromeExecutable,
|
chromeExecutable,
|
||||||
chromeArguments,
|
chromeArguments,
|
||||||
{
|
{
|
||||||
detached: true,
|
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;
|
let chromeClosed = false;
|
||||||
const waitForChromeToClose = new Promise((fulfill, reject) => {
|
const waitForChromeToClose = new Promise((fulfill, reject) => {
|
||||||
@ -136,10 +139,14 @@ class Launcher {
|
|||||||
/** @type {?Connection} */
|
/** @type {?Connection} */
|
||||||
let connection = null;
|
let connection = null;
|
||||||
try {
|
try {
|
||||||
const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
|
|
||||||
const connectionDelay = options.slowMo || 0;
|
const connectionDelay = options.slowMo || 0;
|
||||||
|
if (!options.appMode) {
|
||||||
|
const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
|
||||||
const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, timeout);
|
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);
|
return Browser.create(connection, options, chromeProcess, killChrome);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
forceKillChrome();
|
forceKillChrome();
|
||||||
@ -198,7 +205,7 @@ class Launcher {
|
|||||||
*/
|
*/
|
||||||
static async connect(options = {}) {
|
static async connect(options = {}) {
|
||||||
const connectionDelay = options.slowMo || 0;
|
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'));
|
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 ExecutionContext extends RealExecutionContext {}
|
||||||
export class Page extends RealPage {}
|
export class Page extends RealPage {}
|
||||||
|
|
||||||
|
export interface ConnectionTransport extends NodeJS.EventEmitter {
|
||||||
|
send(string);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChildProcess extends child_process.ChildProcess {}
|
export interface ChildProcess extends child_process.ChildProcess {}
|
||||||
|
10
test/test.js
10
test/test.js
@ -141,6 +141,16 @@ describe('Puppeteer', function() {
|
|||||||
rm(downloadsFolder);
|
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() {
|
describe('Puppeteer.launch', function() {
|
||||||
it('should support ignoreHTTPSErrors option', async({httpsServer}) => {
|
it('should support ignoreHTTPSErrors option', async({httpsServer}) => {
|
||||||
const options = Object.assign({ignoreHTTPSErrors: true}, defaultBrowserOptions);
|
const options = Object.assign({ignoreHTTPSErrors: true}, defaultBrowserOptions);
|
||||||
|
@ -30,6 +30,7 @@ const EXCLUDE_CLASSES = new Set([
|
|||||||
'Multimap',
|
'Multimap',
|
||||||
'NavigatorWatcher',
|
'NavigatorWatcher',
|
||||||
'NetworkManager',
|
'NetworkManager',
|
||||||
|
'Pipe',
|
||||||
'TaskQueue',
|
'TaskQueue',
|
||||||
'WaitTask',
|
'WaitTask',
|
||||||
]);
|
]);
|
||||||
|
Loading…
Reference in New Issue
Block a user