From 15af75f9a24ad9a91ab7cd7a57290e212c8c1c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Fiszer?= Date: Mon, 14 Jan 2019 22:23:34 +0100 Subject: [PATCH] feat(launcher): add browserUrl option to puppeteer.connect (#3558) The `browserURL` option allows to connect to a browser that exposed it's remote debugging protocol on a known port. Fixes #3537 --- docs/api.md | 3 +- lib/Launcher.js | 65 ++++++++++++++++++++++++++++++++++++++++-- lib/Puppeteer.js | 2 +- test/puppeteer.spec.js | 18 ++++++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/docs/api.md b/docs/api.md index 9dde855aa24..4f5be4c8989 100644 --- a/docs/api.md +++ b/docs/api.md @@ -444,7 +444,8 @@ puppeteer.launch().then(async browser => { #### puppeteer.connect(options) - `options` <[Object]> - - `browserWSEndpoint` <[string]> a [browser websocket endpoint](#browserwsendpoint) to connect to. + - `browserWSEndpoint` a [browser websocket endpoint](#browserwsendpoint) to connect to. + - `browserUrl` a browser url to connect to, in format `http://${host}:${port}`. Use interchangeably with `browserWSEndpoint` to let Puppeteer fetch it from [metadata endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target). - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. - `defaultViewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. - `width` <[number]> page width in pixels. diff --git a/lib/Launcher.js b/lib/Launcher.js index 7116205249b..0603d977252 100644 --- a/lib/Launcher.js +++ b/lib/Launcher.js @@ -15,6 +15,8 @@ */ const os = require('os'); const path = require('path'); +const http = require('http'); +const URL = require('url'); const removeFolder = require('rimraf'); const childProcess = require('child_process'); const BrowserFetcher = require('./BrowserFetcher'); @@ -276,18 +278,33 @@ class Launcher { } /** - * @param {!(Launcher.BrowserOptions & {browserWSEndpoint: string, transport?: !Puppeteer.ConnectionTransport})} options + * @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserUrl?: string, transport?: !Puppeteer.ConnectionTransport})} options * @return {!Promise} */ async connect(options) { const { browserWSEndpoint, + browserUrl, ignoreHTTPSErrors = false, defaultViewport = {width: 800, height: 600}, - transport = await WebSocketTransport.create(browserWSEndpoint), + transport, slowMo = 0, } = options; - const connection = new Connection(browserWSEndpoint, transport, slowMo); + + let connectionUrl; + let connectionTransport; + + if (browserWSEndpoint) + connectionUrl = browserWSEndpoint; + else if (!browserWSEndpoint && browserUrl) + connectionUrl = await getWSEndpoint(browserUrl); + + if (transport) + connectionTransport = transport; + else + connectionTransport = await WebSocketTransport.create(connectionUrl); + + const connection = new Connection(connectionUrl, connectionTransport, slowMo); const {browserContextIds} = await connection.send('Target.getBrowserContexts'); return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError)); } @@ -375,6 +392,48 @@ function waitForWSEndpoint(chromeProcess, timeout, preferredRevision) { }); } +/** + * @param {string} browserUrl + * @return {!Promise} + */ +function getWSEndpoint(browserUrl) { + let resolve, reject; + const endpointUrl = URL.resolve(browserUrl, '/json/version'); + const requestOptions = Object.assign(URL.parse(endpointUrl), { method: 'GET' }); + const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + + function handleRequestEnd(data) { + try { + const {webSocketDebuggerUrl} = JSON.parse(data); + resolve(webSocketDebuggerUrl); + } catch (e) { + handleRequestError(e); + } + } + + function handleRequestError(err) { + reject(new Error(`Failed to fetch browser webSocket url from ${endpointUrl}: ${err}`)); + } + + const request = http.request(requestOptions, res => { + let data = ''; + if (res.statusCode !== 200) { + // consume response data to free up memory + res.resume(); + handleRequestError(res.statusCode); + return; + } + res.setEncoding('utf8'); + res.on('data', chunk => data += chunk); + res.on('end', () => handleRequestEnd(data)); + }); + + request.on('error', handleRequestError); + request.end(); + + return promise; +} + /** * @typedef {Object} Launcher.ChromeArgOptions * @property {boolean=} headless diff --git a/lib/Puppeteer.js b/lib/Puppeteer.js index 76660dc37ba..01e3cd02630 100644 --- a/lib/Puppeteer.js +++ b/lib/Puppeteer.js @@ -37,7 +37,7 @@ module.exports = class { } /** - * @param {!(Launcher.BrowserOptions & {browserWSEndpoint: string, transport?: !Puppeteer.ConnectionTransport})} options + * @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserUrl?: string, transport?: !Puppeteer.ConnectionTransport})} options * @return {!Promise} */ connect(options) { diff --git a/test/puppeteer.spec.js b/test/puppeteer.spec.js index 507b8687730..d71ab5ec9fa 100644 --- a/test/puppeteer.spec.js +++ b/test/puppeteer.spec.js @@ -344,6 +344,24 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions}) expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56); await browser.close(); }); + it('should be able to connect using browserUrl, with and without trailing slash', async({server}) => { + const originalBrowser = await puppeteer.launch(Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'] + })); + const browserUrl = 'http://127.0.0.1:21222'; + + const browser1 = await puppeteer.connect({browserUrl}); + const page1 = await browser1.newPage(); + expect(await page1.evaluate(() => 7 * 8)).toBe(56); + browser1.disconnect(); + + const browser2 = await puppeteer.connect({browserUrl: browserUrl + '/'}); + const page2 = await browser2.newPage(); + expect(await page2.evaluate(() => 8 * 7)).toBe(56); + browser2.disconnect(); + + originalBrowser.close(); + }); }); describe('Puppeteer.executablePath', function() { it('should work', async({server}) => {