feat: Introduce BrowserFetcher class (#1983)

This patch introduces `BrowserFetcher` class that manages
downloaded versions of products.

This patch:
- shapes Downloader API to be minimal yet usable for our needs. This
  includes removing such methods as `Downloader.supportedPlatforms` and
  `Downloader.defaultRevision`.
- makes most of the fs-related methods in Downloader async. The only
  exception is the `Downloader.revisionInfo`: it has stay sync due to the
  `pptr.executablePath()` method being sync.
- updates `install.js` and `utils/check_availability.js` to use new API
- finally, renames `Downloader` into `BrowserFetcher`

Fixes #1748.
This commit is contained in:
Andrey Lushnikov 2018-02-07 12:31:53 -05:00 committed by GitHub
parent 18c975509f
commit a363a733b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 260 additions and 141 deletions

View File

@ -10,9 +10,17 @@
- [Environment Variables](#environment-variables) - [Environment Variables](#environment-variables)
- [class: Puppeteer](#class-puppeteer) - [class: Puppeteer](#class-puppeteer)
* [puppeteer.connect(options)](#puppeteerconnectoptions) * [puppeteer.connect(options)](#puppeteerconnectoptions)
* [puppeteer.createBrowserFetcher([options])](#puppeteercreatebrowserfetcheroptions)
* [puppeteer.defaultArgs()](#puppeteerdefaultargs) * [puppeteer.defaultArgs()](#puppeteerdefaultargs)
* [puppeteer.executablePath()](#puppeteerexecutablepath) * [puppeteer.executablePath()](#puppeteerexecutablepath)
* [puppeteer.launch([options])](#puppeteerlaunchoptions) * [puppeteer.launch([options])](#puppeteerlaunchoptions)
- [class: BrowserFetcher](#class-browserfetcher)
* [browserFetcher.canDownload(revision)](#browserfetchercandownloadrevision)
* [browserFetcher.download(revision[, progressCallback])](#browserfetcherdownloadrevision-progresscallback)
* [browserFetcher.localRevisions()](#browserfetcherlocalrevisions)
* [browserFetcher.platform()](#browserfetcherplatform)
* [browserFetcher.remove(revision)](#browserfetcherremoverevision)
* [browserFetcher.revisionInfo(revision)](#browserfetcherrevisioninforevision)
- [class: Browser](#class-browser) - [class: Browser](#class-browser)
* [event: 'disconnected'](#event-disconnected) * [event: 'disconnected'](#event-disconnected)
* [event: 'targetchanged'](#event-targetchanged) * [event: 'targetchanged'](#event-targetchanged)
@ -273,6 +281,13 @@ puppeteer.launch().then(async browser => {
This methods attaches Puppeteer to an existing Chromium instance. This methods attaches Puppeteer to an existing Chromium instance.
#### puppeteer.createBrowserFetcher([options])
- `options` <[Object]>
- `host` <[string]> A download host to be used. Defaults to `https://storage.googleapis.com`.
- `path` <[string]> A path for the downloads folder. Defaults to `<root>/.local-chromium`, where `<root>` is puppeteer's package root.
- `platform` <[string]> Possible values are: `mac`, `win32`, `win64`, `linux`. Defaults to the current platform.
- returns: <[BrowserFetcher]>
#### puppeteer.defaultArgs() #### puppeteer.defaultArgs()
- returns: <[Array]<[string]>> The default flags that Chromium will be launched with. - returns: <[Array]<[string]>> The default flags that Chromium will be launched with.
@ -308,6 +323,63 @@ If Google Chrome (rather than Chromium) is preferred, a [Chrome Canary](https://
> See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description > See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description
of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/lkcr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/lkcr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users.
### class: BrowserFetcher
BrowserFetcher can download and manage different versions of Chromium.
BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from [omahaproxy.appspot.com](http://omahaproxy.appspot.com/).
Example on how to use BrowserFetcher to download a specific version of Chromium and run
puppeteer against it:
```js
const browserFetcher = puppeteer.createBrowserFetcher();
const revisionInfo = await browserFetcher.download('533271');
const browser = await puppeteer.launch({executablePath: revisionInfo.executablePath})
```
> **NOTE** BrowserFetcher is not designed to work concurrently with other
> instances of BrowserFetcher that share the same downloads directory.
#### browserFetcher.canDownload(revision)
- `revision` <[string]> a revision to check availability.
- returns: <[Promise]<[boolean]>> returns `true` if the revision could be downloaded from the host.
The method initiates a HEAD request to check if the revision is available.
#### browserFetcher.download(revision[, progressCallback])
- `revision` <[string]> a revision to download.
- `progressCallback` <[function]([number], [number])> A function that will be called with two arguments:
- `downloadedBytes` <[number]> how many bytes have been downloaded
- `totalBytes` <[number]> how large is the total download.
- returns: <[Promise]<[Object]>> Resolves with revision information when the revision is downloaded and extracted
- `revision` <[string]> the revision the info was created from
- `folderPath` <[string]> path to the extracted revision folder
- `executablePath` <[string]> path to the revision executable
- `url` <[string]> URL this revision can be downloaded from
- `local` <[boolean]> whether the revision is locally available on disk
The method initiates a GET request to download the revision from the host.
#### browserFetcher.localRevisions()
- returns: <[Promise]<[Array]<[string]>>> A list of all revisions available locally on disk.
#### browserFetcher.platform()
- returns: <[string]> Returns one of `mac`, `linux`, `win32` or `win64`.
#### browserFetcher.remove(revision)
- `revision` <[string]> a revision to remove. The method will throw if the revision has not been downloaded.
- returns: <[Promise]> Resolves when the revision has been removed.
#### browserFetcher.revisionInfo(revision)
- `revision` <[string]> a revision to get info for.
- returns: <[Object]>
- `revision` <[string]> the revision the info was created from
- `folderPath` <[string]> path to the extracted revision folder
- `executablePath` <[string]> path to the revision executable
- `url` <[string]> URL this revision can be downloaded from
- `local` <[boolean]> whether the revision is locally available on disk
### class: Browser ### class: Browser
* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) * extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter)
@ -2489,6 +2561,7 @@ reported.
[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String" [string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String"
[stream.Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable "stream.Readable" [stream.Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable "stream.Readable"
[CDPSession]: #class-cdpsession "CDPSession" [CDPSession]: #class-cdpsession "CDPSession"
[BrowserFetcher]: #class-browserfetcher "BrowserFetcher"
[Error]: https://nodejs.org/api/errors.html#errors_class_error "Error" [Error]: https://nodejs.org/api/errors.html#errors_class_error "Error"
[Frame]: #class-frame "Frame" [Frame]: #class-frame "Frame"
[ConsoleMessage]: #class-consolemessage "ConsoleMessage" [ConsoleMessage]: #class-consolemessage "ConsoleMessage"

View File

@ -25,16 +25,16 @@ if (process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || process.env.npm_c
return; return;
} }
const Downloader = require('./lib/Downloader'); const downloadHost = process.env.PUPPETEER_DOWNLOAD_HOST || process.env.npm_config_puppeteer_download_host;
const downloader = Downloader.createDefault();
const platform = downloader.currentPlatform(); const puppeteer = require('./index');
const revision = Downloader.defaultRevision(); const browserFetcher = puppeteer.createBrowserFetcher({ host: downloadHost });
const ProgressBar = require('progress');
const revision = require('./package.json').puppeteer.chromium_revision;
const revisionInfo = browserFetcher.revisionInfo(revision);
const revisionInfo = downloader.revisionInfo(platform, revision);
// Do nothing if the revision is already downloaded. // Do nothing if the revision is already downloaded.
if (revisionInfo.downloaded) if (revisionInfo.local)
return; return;
// Override current environment proxy settings with npm configuration, if any. // Override current environment proxy settings with npm configuration, if any.
@ -49,21 +49,20 @@ if (NPM_HTTP_PROXY)
if (NPM_NO_PROXY) if (NPM_NO_PROXY)
process.env.NO_PROXY = NPM_NO_PROXY; process.env.NO_PROXY = NPM_NO_PROXY;
const allRevisions = downloader.downloadedRevisions(); browserFetcher.download(revisionInfo.revision, onProgress)
const downloadHost = process.env.PUPPETEER_DOWNLOAD_HOST || process.env.npm_config_puppeteer_download_host; .then(() => browserFetcher.localRevisions())
if (downloadHost)
downloader.setDownloadHost(downloadHost);
downloader.downloadRevision(platform, revision, onProgress)
.then(onSuccess) .then(onSuccess)
.catch(onError); .catch(onError);
/** /**
* @param {!Array<string>}
* @return {!Promise} * @return {!Promise}
*/ */
function onSuccess() { function onSuccess(localRevisions) {
console.log('Chromium downloaded to ' + revisionInfo.folderPath); console.log('Chromium downloaded to ' + revisionInfo.folderPath);
localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision);
// Remove previous chromium revisions. // Remove previous chromium revisions.
const cleanupOldVersions = allRevisions.map(({platform, revision}) => downloader.removeRevision(platform, revision)); const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision));
return Promise.all(cleanupOldVersions); return Promise.all(cleanupOldVersions);
} }
@ -77,15 +76,19 @@ function onError(error) {
} }
let progressBar = null; let progressBar = null;
function onProgress(bytesTotal, delta) { let lastDownloadedBytes = 0;
function onProgress(downloadedBytes, totalBytes) {
if (!progressBar) { if (!progressBar) {
progressBar = new ProgressBar(`Downloading Chromium r${revision} - ${toMegabytes(bytesTotal)} [:bar] :percent :etas `, { const ProgressBar = require('progress');
progressBar = new ProgressBar(`Downloading Chromium r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
complete: '=', complete: '=',
incomplete: ' ', incomplete: ' ',
width: 20, width: 20,
total: bytesTotal, total: totalBytes,
}); });
} }
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta); progressBar.tick(delta);
} }

View File

@ -20,6 +20,7 @@ const path = require('path');
const extract = require('extract-zip'); const extract = require('extract-zip');
const util = require('util'); const util = require('util');
const URL = require('url'); const URL = require('url');
const {helper} = require('./helper');
const removeRecursive = require('rimraf'); const removeRecursive = require('rimraf');
// @ts-ignore // @ts-ignore
const ProxyAgent = require('https-proxy-agent'); const ProxyAgent = require('https-proxy-agent');
@ -34,70 +35,52 @@ const downloadURLs = {
win64: '%s/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip', win64: '%s/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip',
}; };
// Project root will be different for node6-transpiled code. const readdirAsync = helper.promisify(fs.readdir.bind(fs));
const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) ? path.join(__dirname, '..') : path.join(__dirname, '..', '..'); const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
class Downloader { function existsAsync(filePath) {
/** let fulfill = null;
* @param {string} downloadsFolder const promise = new Promise(x => fulfill = x);
*/ fs.access(filePath, err => fulfill(!err));
constructor(downloadsFolder) { return promise;
this._downloadsFolder = downloadsFolder;
this._downloadHost = DEFAULT_DOWNLOAD_HOST;
} }
class BrowserFetcher {
/** /**
* @return {string} * @param {!BrowserFetcher.Options=} options
*/ */
static defaultRevision() { constructor(options = {}) {
return require(path.join(PROJECT_ROOT, 'package.json')).puppeteer.chromium_revision; this._downloadsFolder = options.path || path.join(helper.projectRoot(), '.local-chromium');
} this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
this._platform = options.platform || '';
/** if (!this._platform) {
* @return {!Downloader}
*/
static createDefault() {
const downloadsFolder = path.join(PROJECT_ROOT, '.local-chromium');
return new Downloader(downloadsFolder);
}
/**
* @param {string} downloadHost
*/
setDownloadHost(downloadHost) {
this._downloadHost = downloadHost.replace(/\/+$/, '');
}
/**
* @return {!Array<string>}
*/
supportedPlatforms() {
return Object.keys(downloadURLs);
}
/**
* @return {string}
*/
currentPlatform() {
const platform = os.platform(); const platform = os.platform();
if (platform === 'darwin') if (platform === 'darwin')
return 'mac'; this._platform = 'mac';
if (platform === 'linux') else if (platform === 'linux')
return 'linux'; this._platform = 'linux';
if (platform === 'win32') else if (platform === 'win32')
return os.arch() === 'x64' ? 'win64' : 'win32'; this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
return ''; console.assert(this._platform, 'Unsupported platform: ' + os.platform());
}
const supportedPlatforms = ['mac', 'linux', 'win32', 'win64'];
console.assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform);
}
/**
* @return {string}
*/
platform() {
return this._platform;
} }
/** /**
* @param {string} platform
* @param {string} revision * @param {string} revision
* @return {!Promise<boolean>} * @return {!Promise<boolean>}
*/ */
canDownloadRevision(platform, revision) { canDownload(revision) {
console.assert(downloadURLs[platform], 'Unknown platform: ' + platform); const url = util.format(downloadURLs[this._platform], this._downloadHost, revision);
const url = util.format(downloadURLs[platform], this._downloadHost, revision);
let resolve; let resolve;
const promise = new Promise(x => resolve = x); const promise = new Promise(x => resolve = x);
@ -112,90 +95,80 @@ class Downloader {
} }
/** /**
* @param {string} platform
* @param {string} revision * @param {string} revision
* @param {?function(number, number)} progressCallback * @param {?function(number, number)} progressCallback
* @return {!Promise} * @return {!Promise<!BrowserFetcher.RevisionInfo>}
*/ */
downloadRevision(platform, revision, progressCallback) { async download(revision, progressCallback) {
let url = downloadURLs[platform]; let url = downloadURLs[this._platform];
console.assert(url, `Unsupported platform: ${platform}`);
url = util.format(url, this._downloadHost, revision); url = util.format(url, this._downloadHost, revision);
const zipPath = path.join(this._downloadsFolder, `download-${platform}-${revision}.zip`); const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`);
const folderPath = this._getFolderPath(platform, revision); const folderPath = this._getFolderPath(revision);
if (fs.existsSync(folderPath)) if (await existsAsync(folderPath))
return; return this.revisionInfo(revision);
if (!fs.existsSync(this._downloadsFolder)) if (!(await existsAsync(this._downloadsFolder)))
fs.mkdirSync(this._downloadsFolder); await mkdirAsync(this._downloadsFolder);
return downloadFile(url, zipPath, progressCallback) try {
.then(() => extractZip(zipPath, folderPath)) await downloadFile(url, zipPath, progressCallback);
.catch(err => err) await extractZip(zipPath, folderPath);
.then(err => { } finally {
if (fs.existsSync(zipPath)) if (await existsAsync(zipPath))
fs.unlinkSync(zipPath); await unlinkAsync(zipPath);
if (err) }
throw err; return this.revisionInfo(revision);
});
} }
/** /**
* @return {!Array<!{platform:string, revision: string}>} * @return {!Promise<!Array<string>>}
*/ */
downloadedRevisions() { async localRevisions() {
if (!fs.existsSync(this._downloadsFolder)) if (!await existsAsync(this._downloadsFolder))
return []; return [];
const fileNames = fs.readdirSync(this._downloadsFolder); const fileNames = await readdirAsync(this._downloadsFolder);
return fileNames.map(fileName => parseFolderPath(fileName)).filter(revision => !!revision); return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
} }
/** /**
* @param {string} platform
* @param {string} revision * @param {string} revision
* @return {!Promise} * @return {!Promise}
*/ */
removeRevision(platform, revision) { async remove(revision) {
console.assert(downloadURLs[platform], `Unsupported platform: ${platform}`); const folderPath = this._getFolderPath(revision);
const folderPath = this._getFolderPath(platform, revision); console.assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
console.assert(fs.existsSync(folderPath)); await new Promise(fulfill => removeRecursive(folderPath, fulfill));
return new Promise(fulfill => removeRecursive(folderPath, fulfill));
} }
/** /**
* @param {string} platform
* @param {string} revision * @param {string} revision
* @return {!{revision: string, folderPath: string, executablePath: string, downloaded: boolean}} * @return {!BrowserFetcher.RevisionInfo}
*/ */
revisionInfo(platform, revision) { revisionInfo(revision) {
console.assert(downloadURLs[platform], `Unsupported platform: ${platform}`); const folderPath = this._getFolderPath(revision);
const folderPath = this._getFolderPath(platform, revision);
let executablePath = ''; let executablePath = '';
if (platform === 'mac') if (this._platform === 'mac')
executablePath = path.join(folderPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); executablePath = path.join(folderPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
else if (platform === 'linux') else if (this._platform === 'linux')
executablePath = path.join(folderPath, 'chrome-linux', 'chrome'); executablePath = path.join(folderPath, 'chrome-linux', 'chrome');
else if (platform === 'win32' || platform === 'win64') else if (this._platform === 'win32' || this._platform === 'win64')
executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe'); executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe');
else else
throw 'Unsupported platform: ' + platform; throw 'Unsupported platform: ' + this._platform;
return { let url = downloadURLs[this._platform];
revision, url = util.format(url, this._downloadHost, revision);
executablePath, const local = fs.existsSync(folderPath);
folderPath, return {revision, executablePath, folderPath, local, url};
downloaded: fs.existsSync(folderPath)
};
} }
/** /**
* @param {string} platform
* @param {string} revision * @param {string} revision
* @return {string} * @return {string}
*/ */
_getFolderPath(platform, revision) { _getFolderPath(revision) {
return path.join(this._downloadsFolder, platform + '-' + revision); return path.join(this._downloadsFolder, this._platform + '-' + revision);
} }
} }
module.exports = Downloader; module.exports = BrowserFetcher;
/** /**
* @param {string} folderPath * @param {string} folderPath
@ -220,6 +193,8 @@ function parseFolderPath(folderPath) {
*/ */
function downloadFile(url, destinationPath, progressCallback) { function downloadFile(url, destinationPath, progressCallback) {
let fulfill, reject; let fulfill, reject;
let downloadedBytes = 0;
let totalBytes = 0;
const promise = new Promise((x, y) => { fulfill = x; reject = y; }); const promise = new Promise((x, y) => { fulfill = x; reject = y; });
@ -235,15 +210,16 @@ function downloadFile(url, destinationPath, progressCallback) {
file.on('finish', () => fulfill()); file.on('finish', () => fulfill());
file.on('error', error => reject(error)); file.on('error', error => reject(error));
response.pipe(file); response.pipe(file);
const totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10); totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10);
if (progressCallback) if (progressCallback)
response.on('data', onData.bind(null, totalBytes)); response.on('data', onData);
}); });
request.on('error', error => reject(error)); request.on('error', error => reject(error));
return promise; return promise;
function onData(totalBytes, chunk) { function onData(chunk) {
progressCallback(totalBytes, chunk.length); downloadedBytes += chunk.length;
progressCallback(downloadedBytes, totalBytes);
} }
} }
@ -281,3 +257,19 @@ function httpRequest(url, method, response) {
request.end(); request.end();
return request; return request;
} }
/**
* @typedef {Object} BrowserFetcher.Options
* @property {string=} platform
* @property {string=} path
* @property {string=} host
*/
/**
* @typedef {Object} BrowserFetcher.RevisionInfo
* @property {string} folderPath
* @property {string} executablePath
* @property {string} url
* @property {boolean} local
* @property {string} revision
*/

View File

@ -17,13 +17,13 @@ const os = require('os');
const path = require('path'); const path = require('path');
const removeFolder = require('rimraf'); const removeFolder = require('rimraf');
const childProcess = require('child_process'); const childProcess = require('child_process');
const Downloader = require('./Downloader'); const BrowserFetcher = require('./BrowserFetcher');
const {Connection} = require('./Connection'); const {Connection} = require('./Connection');
const {Browser} = require('./Browser'); const {Browser} = require('./Browser');
const readline = require('readline'); const readline = require('readline');
const fs = require('fs'); const fs = require('fs');
const {helper} = require('./helper'); const {helper} = require('./helper');
const ChromiumRevision = Downloader.defaultRevision(); const ChromiumRevision = require(path.join(helper.projectRoot(), 'package.json')).puppeteer.chromium_revision;
const mkdtempAsync = helper.promisify(fs.mkdtemp); const mkdtempAsync = helper.promisify(fs.mkdtemp);
const removeFolderAsync = helper.promisify(removeFolder); const removeFolderAsync = helper.promisify(removeFolder);
@ -90,9 +90,9 @@ class Launcher {
} }
let chromeExecutable = options.executablePath; let chromeExecutable = options.executablePath;
if (typeof chromeExecutable !== 'string') { if (typeof chromeExecutable !== 'string') {
const downloader = Downloader.createDefault(); const browserFetcher = new BrowserFetcher();
const revisionInfo = downloader.revisionInfo(downloader.currentPlatform(), ChromiumRevision); const revisionInfo = browserFetcher.revisionInfo(ChromiumRevision);
console.assert(revisionInfo.downloaded, `Chromium revision is not downloaded. Run "npm install" or "yarn install"`); console.assert(revisionInfo.local, `Chromium revision is not downloaded. Run "npm install" or "yarn install"`);
chromeExecutable = revisionInfo.executablePath; chromeExecutable = revisionInfo.executablePath;
} }
if (Array.isArray(options.args)) if (Array.isArray(options.args))
@ -187,8 +187,8 @@ class Launcher {
* @return {string} * @return {string}
*/ */
static executablePath() { static executablePath() {
const downloader = Downloader.createDefault(); const browserFetcher = new BrowserFetcher();
const revisionInfo = downloader.revisionInfo(downloader.currentPlatform(), ChromiumRevision); const revisionInfo = browserFetcher.revisionInfo(ChromiumRevision);
return revisionInfo.executablePath; return revisionInfo.executablePath;
} }

View File

@ -15,6 +15,7 @@
*/ */
const {helper} = require('./helper'); const {helper} = require('./helper');
const Launcher = require('./Launcher'); const Launcher = require('./Launcher');
const BrowserFetcher = require('./BrowserFetcher');
class Puppeteer { class Puppeteer {
/** /**
@ -46,6 +47,14 @@ class Puppeteer {
static defaultArgs() { static defaultArgs() {
return Launcher.defaultArgs(); return Launcher.defaultArgs();
} }
/**
* @param {!Object=} options
* @return {!BrowserFetcher}
*/
static createBrowserFetcher(options) {
return new BrowserFetcher(options);
}
} }
module.exports = Puppeteer; module.exports = Puppeteer;

View File

@ -13,10 +13,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const fs = require('fs');
const path = require('path');
const debugError = require('debug')(`puppeteer:error`); const debugError = require('debug')(`puppeteer:error`);
/** @type {?Map<string, boolean>} */ /** @type {?Map<string, boolean>} */
let apiCoverage = null; let apiCoverage = null;
let projectRoot = null;
class Helper { class Helper {
/** /**
* @param {Function|string} fun * @param {Function|string} fun
@ -41,6 +44,17 @@ class Helper {
} }
} }
/**
* @return {string}
*/
static projectRoot() {
if (!projectRoot) {
// Project root will be different for node6-transpiled code.
projectRoot = fs.existsSync(path.join(__dirname, '..', 'package.json')) ? path.join(__dirname, '..') : path.join(__dirname, '..', '..');
}
return projectRoot;
}
/** /**
* @param {!Object} exceptionDetails * @param {!Object} exceptionDetails
* @return {string} * @return {string}

Binary file not shown.

View File

@ -185,18 +185,20 @@ class SimpleServer {
if (this._requestSubscribers.has(pathName)) if (this._requestSubscribers.has(pathName))
this._requestSubscribers.get(pathName)[fulfillSymbol].call(null, request); this._requestSubscribers.get(pathName)[fulfillSymbol].call(null, request);
const handler = this._routes.get(pathName); const handler = this._routes.get(pathName);
if (handler) if (handler) {
handler.call(null, request, response); handler.call(null, request, response);
else } else {
this.defaultHandler(request, response); const pathName = url.parse(request.url).path;
this.serveFile(request, response, pathName);
}
} }
/** /**
* @param {!IncomingMessage} request * @param {!IncomingMessage} request
* @param {!ServerResponse} response * @param {!ServerResponse} response
* @param {string} pathName
*/ */
defaultHandler(request, response) { serveFile(request, response, pathName) {
let pathName = url.parse(request.url).path;
if (pathName === '/') if (pathName === '/')
pathName = '/index.html'; pathName = '/index.html';
const filePath = path.join(this._dirPath, pathName.substring(1)); const filePath = path.join(this._dirPath, pathName.substring(1));

View File

@ -21,6 +21,7 @@ const {helper} = require('../lib/helper');
if (process.env.COVERAGE) if (process.env.COVERAGE)
helper.recordPublicAPICoverage(); helper.recordPublicAPICoverage();
const mkdtempAsync = helper.promisify(fs.mkdtemp); const mkdtempAsync = helper.promisify(fs.mkdtemp);
const readFileAsync = helper.promisify(fs.readFile);
const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) ? path.join(__dirname, '..') : path.join(__dirname, '..', '..'); const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) ? path.join(__dirname, '..') : path.join(__dirname, '..', '..');
@ -113,6 +114,33 @@ afterAll(async({server, httpsServer}) => {
}); });
describe('Puppeteer', function() { describe('Puppeteer', function() {
describe('BrowserFetcher', function() {
it('should download and extract linux binary', async({server}) => {
const downloadsFolder = await mkdtempAsync(TMP_FOLDER);
const browserFetcher = puppeteer.createBrowserFetcher({
platform: 'linux',
path: downloadsFolder,
host: server.PREFIX
});
let revisionInfo = browserFetcher.revisionInfo('123456');
server.setRoute(revisionInfo.url.substring(server.PREFIX.length), (req, res) => {
server.serveFile(req, res, '/chromium-linux.zip');
});
expect(revisionInfo.local).toBe(false);
expect(browserFetcher.platform()).toBe('linux');
expect(await browserFetcher.canDownload('100000')).toBe(false);
expect(await browserFetcher.canDownload('123456')).toBe(true);
revisionInfo = await browserFetcher.download('123456');
expect(revisionInfo.local).toBe(true);
expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe('LINUX BINARY\n');
expect(await browserFetcher.localRevisions()).toEqual(['123456']);
await browserFetcher.remove('123456');
expect(await browserFetcher.localRevisions()).toEqual([]);
rm(downloadsFolder);
});
});
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);

View File

@ -15,11 +15,12 @@
* limitations under the License. * limitations under the License.
*/ */
const Downloader = require('../lib/Downloader'); const puppeteer = require('..');
const https = require('https'); const https = require('https');
const OMAHA_PROXY = 'https://omahaproxy.appspot.com/all.json'; const OMAHA_PROXY = 'https://omahaproxy.appspot.com/all.json';
const SUPPORTER_PLATFORMS = ['linux', 'mac', 'win32', 'win64'];
const downloader = Downloader.createDefault(); const fetchers = SUPPORTER_PLATFORMS.map(platform => puppeteer.createBrowserFetcher({platform}));
const colors = { const colors = {
reset: '\x1b[0m', reset: '\x1b[0m',
@ -73,7 +74,7 @@ async function checkOmahaProxyAvailability() {
return; return;
} }
const table = new Table([27, 7, 7, 7, 7]); const table = new Table([27, 7, 7, 7, 7]);
table.drawRow([''].concat(downloader.supportedPlatforms())); table.drawRow([''].concat(SUPPORTER_PLATFORMS));
for (const platform of platforms) { for (const platform of platforms) {
// Trust only to the main platforms. // Trust only to the main platforms.
if (platform.os !== 'mac' && platform.os !== 'win' && platform.os !== 'win64' && platform.os !== 'linux') if (platform.os !== 'mac' && platform.os !== 'win' && platform.os !== 'win64' && platform.os !== 'linux')
@ -95,7 +96,7 @@ async function checkOmahaProxyAvailability() {
*/ */
async function checkRangeAvailability(fromRevision, toRevision) { async function checkRangeAvailability(fromRevision, toRevision) {
const table = new Table([10, 7, 7, 7, 7]); const table = new Table([10, 7, 7, 7, 7]);
table.drawRow([''].concat(downloader.supportedPlatforms())); table.drawRow([''].concat(SUPPORTER_PLATFORMS));
const inc = fromRevision < toRevision ? 1 : -1; const inc = fromRevision < toRevision ? 1 : -1;
for (let revision = fromRevision; revision !== toRevision; revision += inc) for (let revision = fromRevision; revision !== toRevision; revision += inc)
await checkAndDrawRevisionAvailability(table, '', revision); await checkAndDrawRevisionAvailability(table, '', revision);
@ -107,9 +108,7 @@ async function checkRangeAvailability(fromRevision, toRevision) {
* @param {number} revision * @param {number} revision
*/ */
async function checkAndDrawRevisionAvailability(table, name, revision) { async function checkAndDrawRevisionAvailability(table, name, revision) {
const promises = []; const promises = fetchers.map(fetcher => fetcher.canDownload(revision));
for (const platform of downloader.supportedPlatforms())
promises.push(downloader.canDownloadRevision(platform, revision));
const availability = await Promise.all(promises); const availability = await Promise.all(promises);
const allAvailable = availability.every(e => !!e); const allAvailable = availability.every(e => !!e);
const values = [name + ' ' + (allAvailable ? colors.green + revision + colors.reset : revision)]; const values = [name + ' ' + (allAvailable ? colors.green + revision + colors.reset : revision)];

View File

@ -22,7 +22,6 @@ const Message = require('../Message');
const EXCLUDE_CLASSES = new Set([ const EXCLUDE_CLASSES = new Set([
'CSSCoverage', 'CSSCoverage',
'Connection', 'Connection',
'Downloader',
'EmulationManager', 'EmulationManager',
'FrameManager', 'FrameManager',
'JSCoverage', 'JSCoverage',