Use puppeteer.launch instead of browser constructor (#255)

This patch:
- split browser launching logic from Browser into `lib/Launcher.js`
- introduce `puppeteer` namespace which currently has a single `launch`
  method to start a browser

With this patch, the browser is no longer created with the `new
Browser(..)` command. Instead, it should be "launched" via the
`puppeteer.launch` method:

```js
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
  ...
});
```

With this approach browser instance lifetime matches the lifetime of
actual browser process. This helps us:
- remove proxy streams, e.g. browser.stderr and browser.stdout
- cleanup browser class and make it possible to connect to remote
  browser
- introduce events on the browser instance, e.g. 'page' event. In case
  of lazy-launching browser, we should've launch browser when an event
  listener is added, which is unneded comlpexity.
This commit is contained in:
Andrey Lushnikov 2017-08-14 18:08:06 -07:00 committed by GitHub
parent 0a3dd4e727
commit 13e8580a34
19 changed files with 301 additions and 291 deletions

View File

@ -133,11 +133,11 @@ npm run coverage
Puppeteer uses [DEBUG](https://github.com/visionmedia/debug) module to expose some of it's inner guts under the `puppeteer` namespace. Try putting the following in a file called `script.js` and running it via `DEBUG=* node script.js`:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
browser.close();

View File

@ -39,10 +39,11 @@ of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https:
**Example** - navigating to https://example.com and saving a screenshot as *example.png*:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({path: 'example.png'});
@ -54,16 +55,17 @@ browser.close();
or, without `async`/`await`:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
browser.newPage().then(page => {
page.goto('https://example.com').then(response => {
page.screenshot({path: 'example.png'}).then(buffer => {
browser.close();
});
puppeteer.launch()
.then(browser => browser.newPage())
.then(page => {
page.goto('https://example.com').then(response => {
page.screenshot({path: 'example.png'}).then(buffer => {
browser.close();
});
});
});
});
```
Puppeteer sets an initial page size to 800px x 600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagesetviewportviewport).
@ -71,10 +73,11 @@ Puppeteer sets an initial page size to 800px x 600px, which defines the screensh
**Example** - create a PDF.
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle'});
await page.pdf({path: 'hn.pdf', format: 'A4'});
@ -92,7 +95,7 @@ See [`Page.pdf()`](https://github.com/GoogleChrome/puppeteer/blob/master/docs/ap
Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the ['headless' option](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#new-browseroptions) when creating a browser:
```js
const browser = new Browser({headless: false});
const browser = await puppeteer.launch({headless: false});
```
**2. Runs a bundled version of Chromium**

View File

@ -5,12 +5,11 @@
<!-- toc -->
- [Puppeteer](#puppeteer)
* [class: Puppeteer](#class-puppeteer)
+ [puppeteer.launch([options])](#puppeteerlaunchoptions)
* [class: Browser](#class-browser)
+ [new Browser([options])](#new-browseroptions)
+ [browser.close()](#browserclose)
+ [browser.newPage()](#browsernewpage)
+ [browser.stderr](#browserstderr)
+ [browser.stdout](#browserstdout)
+ [browser.version()](#browserversion)
* [class: Page](#class-page)
+ [event: 'console'](#event-console)
@ -120,18 +119,33 @@
Puppeteer is a Node library which provides a high-level API to control Chromium over the DevTools Protocol.
Puppeteer provides a top-level require which has a [Browser](#class-browser) class.
The following is a typical example of using a Browser class to drive automation:
### class: Puppeteer
Puppeteer module provides a method to launch a chromium instance.
The following is a typical example of using a Puppeteer to drive automation:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page => {
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
await page.goto('https://google.com');
// other actions...
browser.close();
});
```
#### puppeteer.launch([options])
- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields:
- `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
- `headless` <[boolean]> Whether to run chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true`.
- `executablePath` <[string]> Path to a chromium executable to run instead of bundled chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd).
- `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful so that you can see what is going on.
- `args` <[Array]<[string]>> Additional arguments to pass to the chromium instance. List of chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/).
- `dumpio` <[boolean]> Whether to pipe browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
- returns: <[Promise]<[Browser]>> Promise which resolves to browser instance.
The method launches a browser instance with given arguments. The browser will be closed when the parent node.js process gets closed.
### class: Browser
Browser manages a browser instance, creating it with a predefined
@ -140,23 +154,15 @@ not necessarily result in launching browser; the instance will be launched when
A typical scenario of using [Browser] is opening a new page and navigating it to a desired URL:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page => {
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
await page.goto('https://example.com');
browser.close();
});
```
#### new Browser([options])
- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields:
- `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
- `headless` <[boolean]> Whether to run chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true`.
- `executablePath` <[string]> Path to a chromium executable to run instead of bundled chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd).
- `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful
so that you can see what is going on.
- `args` <[Array]<[string]>> Additional arguments to pass to the chromium instance. List of chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/).
#### browser.close()
@ -166,36 +172,6 @@ Closes browser with all the pages (if any were opened). The browser object itsel
- returns: <[Promise]<[Page]>> Promise which resolves to a new [Page] object.
#### browser.stderr
- <[stream.Readable]>
A Readable Stream that represents the browser process's stderr.
For example, `stderr` could be piped into `process.stderr`:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.stderr.pipe(process.stderr);
browser.version().then(version => {
console.log(version);
browser.close();
});
```
#### browser.stdout
- <[stream.Readable]>
A Readable Stream that represents the browser process's stdout.
For example, `stdout` could be piped into `process.stdout`:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.stdout.pipe(process.stdout);
browser.version().then(version => {
console.log(version);
browser.close();
});
```
#### browser.version()
- returns: <[Promise]<[string]>> String describing browser version. For headless chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For non-headless, this is `Chrome/61.0.3153.0`.
@ -207,9 +183,10 @@ Page provides methods to interact with browser page. Page could be thought about
An example of creating a page, navigating it to a URL and saving screenshot as `screenshot.png`:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page =>
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({path: 'screenshot.png'});
browser.close();
@ -298,11 +275,11 @@ If the `puppeteerFunction` returns a promise, it would be awaited.
An example of adding `window.md5` binding to the page:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
const crypto = require('crypto');
browser.newPage().then(async page => {
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
page.on('console', console.log);
await page.setInPageCallback('md5', text => crypto.createHash('md5').update(text).digest('hex'));
await page.evaluate(async () => {
@ -318,11 +295,11 @@ browser.newPage().then(async page => {
An example of adding `window.readfile` binding to the page:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
const fs = require('fs');
browser.newPage().then(async page => {
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
page.on('console', console.log);
await page.setInPageCallback('readfile', async filePath => {
return new Promise((resolve, reject) => {
@ -380,11 +357,12 @@ Emulates given device metrics and user agent. This method is a shortcut for call
To aid emulation, puppeteer provides a list of device descriptors which could be obtained via the `require('puppeteer/DeviceDescriptors')` command.
Below is an example of emulating iPhone 6 in puppeteer:
```js
const {Browser} = require('puppeteer');
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];
const browser = new Browser();
browser.newPage().then(async page => {
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
await page.emulate(iPhone);
await page.goto('https://google.com');
// other actions...
@ -402,9 +380,9 @@ List of all available devices is available in the source code: [DeviceDescriptor
If the function, passed to the `page.evaluate`, returns a [Promise], then `page.evaluate` would wait for the promise to resolve and return it's value.
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page =>
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
const result = await page.evaluate(() => {
return Promise.resolve(8 * 7);
});
@ -613,10 +591,9 @@ Activating request interception enables `request.abort` and `request.continue`.
An example of a naïve request interceptor which aborts all image requests:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page =>
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
await page.setRequestInterceptionEnabled(true);
page.on('request', request => {
if (interceptedRequest.url.endsWith('.png') || interceptedRequest.url.endsWith('.jpg'))
@ -713,10 +690,9 @@ Shortcut for [page.mainFrame().waitFor(selectorOrFunctionOrTimeout[, options])](
The `waitForFunction` could be used to observe viewport size change:
```js
const {Browser} = require('.');
const browser = new Browser();
browser.newPage().then(async page => {
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
const watchDog = page.waitForFunction('window.innerWidth < 100');
page.setViewport({width: 50, height: 50});
await watchDog;
@ -748,10 +724,9 @@ immediately. If the selector doesn't appear after the `timeout` milliseconds of
This method works across navigations:
```js
const {Browser} = new require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page => {
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
let currentURL;
page.waitForSelector('img').then(() => console.log('First URL with image: ' + currentURL));
for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
@ -874,9 +849,10 @@ Only one trace can be active at a time per browser.
An example of using `Dialog` class:
```js
const {Browser} = require('puppeteer');
const browser = new Browser({headless: false});
browser.newPage().then(async page => {
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
page.on('dialog', dialog => {
console.log(dialog.message());
dialog.dismiss();
@ -916,10 +892,10 @@ At every point of time, page exposes its current frame tree via the [page.mainFr
An example of dumping frame tree:
```js
const {Browser} = new require('.');
const browser = new Browser({headless: true});
const puppeteer = require('puppeteer');
browser.newPage().then(async page => {
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
await page.goto('https://www.google.com/chrome/browser/canary.html');
dumpFrameTree(page.mainFrame(), '');
browser.close();
@ -958,9 +934,10 @@ Adds a `<script>` tag to the frame with the desired url. Alternatively, JavaScri
If the function, passed to the `page.evaluate`, returns a [Promise], then `page.evaluate` would wait for the promise to resolve and return it's value.
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page =>
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
const result = await page.evaluate(() => {
return Promise.resolve(8 * 7);
});
@ -1042,10 +1019,10 @@ This method behaves differently with respect to the type of the first parameter:
The `waitForFunction` could be used to observe viewport size change:
```js
const {Browser} = require('.');
const browser = new Browser();
const puppeteer = require('puppeteer');
browser.newPage().then(async page => {
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
const watchDog = page.waitForFunction('window.innerWidth < 100');
page.setViewport({width: 50, height: 50});
await watchDog;
@ -1066,10 +1043,10 @@ immediately. If the selector doesn't appear after the `timeout` milliseconds of
This method works across navigations:
```js
const {Browser} = new require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
browser.newPage().then(async page => {
puppeteer.launch().then(async browser => {
let page = await browser.newPage();
let currentURL;
page.waitForSelector('img').then(() => console.log('First URL with image: ' + currentURL));
for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])

View File

@ -14,13 +14,14 @@
* limitations under the License.
*/
const {Browser} = require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setRequestInterceptor(request => {
await page.setRequestInterceptionEnabled(true);
page.on('request', request => {
if (/\.(png|jpg|jpeg$)/.test(request.url))
request.abort();
else

View File

@ -14,8 +14,7 @@
* limitations under the License.
*/
const {Browser} = require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
function sniffDetector() {
let userAgent = window.navigator.userAgent;
@ -34,6 +33,7 @@ function sniffDetector() {
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.evaluateOnNewDocument(sniffDetector);
await page.goto('https://www.google.com', {waitUntil: 'networkidle'});
@ -41,4 +41,4 @@ console.log('Sniffed: ' + (await page.evaluate(() => !!navigator.sniffed)));
browser.close();
})();
})();

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
const {Browser} = require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle'});
// page.pdf() is currently supported only in headless mode.

View File

@ -14,12 +14,12 @@
* limitations under the License.
*/
const {Browser} = require('puppeteer');
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const browser = new Browser();
(async() => {
const browser = await puppeteer.launch();
let page = await browser.newPage();
await page.emulate(devices['iPhone 6']);
await page.goto('https://www.nytimes.com/');

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
const {Browser} = require('puppeteer');
const browser = new Browser();
const puppeteer = require('puppeteer');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://example.com');
await page.screenshot({path: 'example.png'});

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
const puppeteer = require('puppeteer');
(async() => {
const {Browser} = require('puppeteer');
const browser = new Browser();
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://google.com', {waitUntil: 'networkidle'});
// Type our query into the search bar

View File

@ -14,6 +14,4 @@
* limitations under the License.
*/
module.exports = {
Browser: require('./lib/Browser')
};
module.exports = require('./lib/Puppeteer');

View File

@ -14,85 +14,26 @@
* limitations under the License.
*/
const {Duplex} = require('stream');
const path = require('path');
const helper = require('./helper');
const removeRecursive = require('rimraf').sync;
const Page = require('./Page');
const childProcess = require('child_process');
const Downloader = require('../utils/ChromiumDownloader');
const Connection = require('./Connection');
const readline = require('readline');
const CHROME_PROFILE_PATH = path.resolve(__dirname, '..', '.dev_profile');
let browserId = 0;
const DEFAULT_ARGS = [
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-client-side-phishing-detection',
'--disable-default-apps',
'--disable-hang-monitor',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-sync',
'--enable-automation',
'--metrics-recording-only',
'--no-first-run',
'--password-store=basic',
'--remote-debugging-port=0',
'--safebrowsing-disable-auto-update',
'--use-mock-keychain',
];
class Browser {
/**
* @param {!Object=} options
* @param {!Connection} connection
* @param {boolean} ignoreHTTPSErrors
* @param {function()=} closeCallback
*/
constructor(options) {
options = options || {};
++browserId;
this._userDataDir = CHROME_PROFILE_PATH + browserId;
this._remoteDebuggingPort = 0;
this._chromeArguments = DEFAULT_ARGS.concat([
`--user-data-dir=${this._userDataDir}`,
]);
if (typeof options.headless !== 'boolean' || options.headless) {
this._chromeArguments.push(
`--headless`,
`--disable-gpu`,
`--hide-scrollbars`
);
}
if (typeof options.executablePath === 'string') {
this._chromeExecutable = options.executablePath;
} else {
let chromiumRevision = require('../package.json').puppeteer.chromium_revision;
let revisionInfo = Downloader.revisionInfo(Downloader.currentPlatform(), chromiumRevision);
console.assert(revisionInfo, 'Chromium revision is not downloaded. Run npm install');
this._chromeExecutable = revisionInfo.executablePath;
}
this._ignoreHTTPSErrors = !!options.ignoreHTTPSErrors;
if (Array.isArray(options.args))
this._chromeArguments.push(...options.args);
this._connectionDelay = options.slowMo || 0;
this._terminated = false;
this._chromeProcess = null;
this._launchPromise = null;
constructor(connection, ignoreHTTPSErrors, closeCallback) {
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._screenshotTaskQueue = new TaskQueue();
this.stderr = new ProxyStream();
this.stdout = new ProxyStream();
this._connection = connection;
this._closeCallback = closeCallback || new Function();
}
/**
* @return {!Promise<!Page>}
*/
async newPage() {
await this._ensureChromeIsRunning();
if (!this._chromeProcess || this._terminated)
throw new Error('ERROR: this chrome instance is not alive any more!');
const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank'});
const client = await this._connection.createSession(targetId);
return await Page.create(client, this._ignoreHTTPSErrors, this._screenshotTaskQueue);
@ -102,84 +43,19 @@ class Browser {
* @return {!Promise<string>}
*/
async version() {
await this._ensureChromeIsRunning();
let version = await this._connection.send('Browser.getVersion');
return version.product;
}
/**
* @return {!Promise}
*/
async _ensureChromeIsRunning() {
if (!this._launchPromise)
this._launchPromise = this._launchChrome();
return this._launchPromise;
}
/**
* @return {!Promise}
*/
async _launchChrome() {
this._chromeProcess = childProcess.spawn(this._chromeExecutable, this._chromeArguments, {});
let stderr = '';
this._chromeProcess.stderr.on('data', data => stderr += data.toString('utf8'));
// Cleanup as processes exit.
const onProcessExit = () => this._chromeProcess.kill();
process.on('exit', onProcessExit);
this._chromeProcess.on('exit', () => {
this._terminated = true;
process.removeListener('exit', onProcessExit);
removeRecursive(this._userDataDir);
});
this._chromeProcess.stderr.pipe(this.stderr);
this._chromeProcess.stdout.pipe(this.stdout);
let {port, browserTargetId} = await waitForRemoteDebuggingPort(this._chromeProcess);
// Failed to connect to browser.
if (port === -1) {
this._chromeProcess.kill();
throw new Error('Failed to connect to chrome!');
}
if (this._terminated)
throw new Error('Failed to launch chrome! ' + stderr);
this._remoteDebuggingPort = port;
this._connection = await Connection.create(port, browserTargetId, this._connectionDelay);
}
close() {
if (!this._chromeProcess)
return;
this._chromeProcess.kill();
this._connection.dispose();
this._closeCallback.call(null);
}
}
module.exports = Browser;
helper.tracePublicAPI(Browser);
/**
* @param {!ChildProcess} chromeProcess
* @return {!Promise<number>}
*/
function waitForRemoteDebuggingPort(chromeProcess) {
return new Promise(fulfill => {
const rl = readline.createInterface({ input: chromeProcess.stderr });
rl.on('line', onLine);
rl.once('close', () => fulfill(-1));
/**
* @param {string} line
*/
function onLine(line) {
const match = line.match(/^DevTools listening on .*:(\d+)(\/.*)$/);
if (!match)
return;
rl.removeListener('line', onLine);
fulfill({port: Number.parseInt(match[1], 10), browserTargetId: match[2]});
}
});
}
class TaskQueue {
constructor() {
this._chain = Promise.resolve();
@ -195,17 +71,3 @@ class TaskQueue {
return result;
}
}
class ProxyStream extends Duplex {
_read() { }
/**
* @param {?} chunk
* @param {string} encoding
* @param {function()} callback
*/
_write(chunk, encoding, callback) {
this.push(chunk, encoding);
callback();
}
}

View File

@ -99,7 +99,8 @@ class Connection extends EventEmitter {
/**
* @return {!Promise}
*/
async dispose() {
dispose() {
this._onClose();
this._ws.close();
}

128
lib/Launcher.js Normal file
View File

@ -0,0 +1,128 @@
/**
* Copyright 2017 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 path = require('path');
const removeRecursive = require('rimraf').sync;
const childProcess = require('child_process');
const Downloader = require('../utils/ChromiumDownloader');
const Connection = require('./Connection');
const Browser = require('./Browser');
const readline = require('readline');
const CHROME_PROFILE_PATH = path.resolve(__dirname, '..', '.dev_profile');
let browserId = 0;
const DEFAULT_ARGS = [
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-client-side-phishing-detection',
'--disable-default-apps',
'--disable-hang-monitor',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-sync',
'--enable-automation',
'--metrics-recording-only',
'--no-first-run',
'--password-store=basic',
'--remote-debugging-port=0',
'--safebrowsing-disable-auto-update',
'--use-mock-keychain',
];
class Launcher {
/**
* @param {!Object} options
* @return {!Promise<!Browser>}
*/
static async launch(options) {
options = options || {};
++browserId;
let userDataDir = CHROME_PROFILE_PATH + browserId;
let chromeArguments = DEFAULT_ARGS.concat([
`--user-data-dir=${userDataDir}`,
]);
if (typeof options.headless !== 'boolean' || options.headless) {
chromeArguments.push(
`--headless`,
`--disable-gpu`,
`--hide-scrollbars`
);
}
let chromeExecutable = options.executablePath;
if (typeof chromeExecutable !== 'string') {
let chromiumRevision = require('../package.json').puppeteer.chromium_revision;
let revisionInfo = Downloader.revisionInfo(Downloader.currentPlatform(), chromiumRevision);
console.assert(revisionInfo, 'Chromium revision is not downloaded. Run npm install');
chromeExecutable = revisionInfo.executablePath;
}
if (Array.isArray(options.args))
chromeArguments.push(...options.args);
let chromeProcess = childProcess.spawn(chromeExecutable, chromeArguments, {});
if (options.dumpio) {
chromeProcess.stdout.pipe(process.stdout);
chromeProcess.stderr.pipe(process.stderr);
}
let stderr = '';
chromeProcess.stderr.on('data', data => stderr += data.toString('utf8'));
// Cleanup as processes exit.
const onProcessExit = () => chromeProcess.kill();
process.on('exit', onProcessExit);
let terminated = false;
chromeProcess.on('exit', () => {
terminated = true;
process.removeListener('exit', onProcessExit);
removeRecursive(userDataDir);
});
let {port, browserTargetId} = await waitForRemoteDebuggingPort(chromeProcess);
if (terminated)
throw new Error('Failed to launch chrome! ' + stderr);
// Failed to connect to browser.
if (port === -1) {
chromeProcess.kill();
throw new Error('Failed to connect to chrome!');
}
let connectionDelay = options.slowMo || 0;
let connection = await Connection.create(port, browserTargetId, connectionDelay);
return new Browser(connection, !!options.ignoreHTTPSErrors, () => chromeProcess.kill());
}
}
/**
* @param {!ChildProcess} chromeProcess
* @return {!Promise<number>}
*/
function waitForRemoteDebuggingPort(chromeProcess) {
return new Promise(fulfill => {
const rl = readline.createInterface({ input: chromeProcess.stderr });
rl.on('line', onLine);
rl.once('close', () => fulfill(-1));
/**
* @param {string} line
*/
function onLine(line) {
const match = line.match(/^DevTools listening on .*:(\d+)(\/.*)$/);
if (!match)
return;
rl.removeListener('line', onLine);
fulfill({port: Number.parseInt(match[1], 10), browserTargetId: match[2]});
}
});
}
module.exports = Launcher;

30
lib/Puppeteer.js Normal file
View File

@ -0,0 +1,30 @@
/**
* Copyright 2017 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 Launcher = require('./Launcher');
class Puppeteer {
/**
* @param {!Object=} options
* @return {!Promise<!Browser>}
*/
static async launch(options) {
return Launcher.launch(options);
}
}
module.exports = Puppeteer;
helper.tracePublicAPI(Puppeteer);

View File

@ -15,6 +15,7 @@
* limitations under the License.
*/
const await = require('./utilities').await;
const vm = require('vm');
const path = require('path');
const fs = require('fs');
@ -24,7 +25,7 @@ const System = require('./System');
const WebPage = require('./WebPage');
const WebServer = require('./WebServer');
const child_process = require('child_process');
const Browser = require('..').Browser;
const puppeteer = require('..');
const argv = require('minimist')(process.argv.slice(2), {
alias: { v: 'version' },
boolean: ['headless'],
@ -55,23 +56,19 @@ if (!fs.existsSync(scriptPath)) {
return;
}
let browser = new Browser({
headless: argv.headless,
args: ['--no-sandbox']
});
let context = createPhantomContext(browser, scriptPath, argv);
let context = createPhantomContext(argv.headless, scriptPath, argv);
let scriptContent = fs.readFileSync(scriptPath, 'utf8');
vm.runInContext(scriptContent, context);
/**
* @param {!Browser} browser
* @param {boolean} headless
* @param {string} scriptPath
* @param {!Array<string>} argv
* @return {!Object}
*/
function createPhantomContext(browser, scriptPath, argv) {
function createPhantomContext(headless, scriptPath, argv) {
let context = {};
let browser = null;
context.setInterval = setInterval;
context.setTimeout = setTimeout;
context.clearInterval = clearInterval;
@ -80,7 +77,7 @@ function createPhantomContext(browser, scriptPath, argv) {
context.phantom = Phantom.create(context, scriptPath);
context.console = console;
context.window = context;
context.WebPage = options => new WebPage(browser, scriptPath, options);
context.WebPage = options => new WebPage(ensureBrowser(), scriptPath, options);
vm.createContext(context);
@ -104,5 +101,15 @@ function createPhantomContext(browser, scriptPath, argv) {
filename: 'bootstrap.js'
})(nativeExports);
return context;
function ensureBrowser() {
if (!browser) {
browser = await(puppeteer.launch({
headless: argv.headless,
args: ['--no-sandbox']
}));
}
return browser;
}
}

View File

@ -20,7 +20,7 @@ const path = require('path');
const helper = require('../lib/helper');
if (process.env.COVERAGE)
helper.recordPublicAPICoverage();
const Browser = require('../lib/Browser');
const puppeteer = require('..');
const SimpleServer = require('./server/SimpleServer');
const GoldenUtils = require('./golden-utils');
@ -82,7 +82,7 @@ afterAll(SX(async function() {
describe('Browser', function() {
it('Browser.Options.ignoreHTTPSErrors', SX(async function() {
let options = Object.assign({ignoreHTTPSErrors: true}, defaultBrowserOptions);
let browser = new Browser(options);
let browser = await puppeteer.launch(options);
let page = await browser.newPage();
let error = null;
let response = null;
@ -96,7 +96,7 @@ describe('Browser', function() {
browser.close();
}));
it('should reject all promises when browser is closed', SX(async function() {
let browser = new Browser(defaultBrowserOptions);
let browser = await puppeteer.launch(defaultBrowserOptions);
let page = await browser.newPage();
let error = null;
let neverResolves = page.evaluate(() => new Promise(r => {})).catch(e => error = e);
@ -114,7 +114,7 @@ describe('Page', function() {
let page;
beforeAll(SX(async function() {
browser = new Browser(defaultBrowserOptions);
browser = await puppeteer.launch(defaultBrowserOptions);
}));
afterAll(SX(async function() {

View File

@ -24,6 +24,7 @@ const EXCLUDE_CLASSES = new Set([
'EmulationManager',
'FrameManager',
'Helper',
'Launcher',
'Multimap',
'NavigatorWatcher',
'NetworkManager',
@ -35,6 +36,7 @@ const EXCLUDE_CLASSES = new Set([
const EXCLUDE_METHODS = new Set([
'Body.constructor',
'Browser.constructor',
'Dialog.constructor',
'Frame.constructor',
'Headers.constructor',

View File

@ -17,7 +17,7 @@
const fs = require('fs');
const rm = require('rimraf').sync;
const path = require('path');
const Browser = require('../../../../lib/Browser');
const puppeteer = require('../../../..');
const checkPublicAPI = require('..');
const SourceFactory = require('../../SourceFactory');
const GoldenUtils = require('../../../../test/golden-utils');
@ -25,7 +25,7 @@ const GoldenUtils = require('../../../../test/golden-utils');
const OUTPUT_DIR = path.join(__dirname, 'output');
const GOLDEN_DIR = path.join(__dirname, 'golden');
const browser = new Browser({args: ['--no-sandbox']});
let browser;
let page;
let specName;
@ -34,6 +34,7 @@ jasmine.getEnv().addReporter({
});
beforeAll(SX(async function() {
browser = await puppeteer.launch({args: ['--no-sandbox']});
page = await browser.newPage();
if (fs.existsSync(OUTPUT_DIR))
rm(OUTPUT_DIR);

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
const Browser = require('../../lib/Browser');
const puppeteer = require('../..');
const path = require('path');
const SourceFactory = require('./SourceFactory');
@ -44,7 +44,7 @@ async function run() {
const preprocessor = require('./preprocessor');
messages.push(...await preprocessor(mdSources));
const browser = new Browser({args: ['--no-sandbox']});
const browser = await puppeteer.launch({args: ['--no-sandbox']});
const page = await browser.newPage();
const checkPublicAPI = require('./check_public_api');
const jsSources = await sourceFactory.readdir(path.join(PROJECT_DIR, 'lib'), '.js');