2019-11-18 18:18:28 -08:00
|
|
|
/**
|
|
|
|
* Copyright 2017 Google Inc. All rights reserved.
|
|
|
|
* Modifications copyright (c) Microsoft Corporation.
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import * as extract from 'extract-zip';
|
|
|
|
import * as fs from 'fs';
|
2020-06-10 18:49:03 -07:00
|
|
|
import * as os from 'os';
|
|
|
|
import * as path from 'path';
|
2020-04-24 19:14:10 -07:00
|
|
|
import * as ProgressBar from 'progress';
|
2019-11-18 18:18:28 -08:00
|
|
|
import { getProxyForUrl } from 'proxy-from-env';
|
|
|
|
import * as URL from 'url';
|
2020-04-24 19:14:10 -07:00
|
|
|
import * as util from 'util';
|
2020-08-22 07:07:13 -07:00
|
|
|
import { assert, getFromENV } from '../utils/utils';
|
2020-08-22 21:15:03 -07:00
|
|
|
import * as browserPaths from '../utils/browserPaths';
|
|
|
|
import { BrowserName, BrowserPlatform, BrowserDescriptor } from '../utils/browserPaths';
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2020-06-30 17:03:01 -07:00
|
|
|
// `https-proxy-agent` v5 is written in Typescript and exposes generated types.
|
|
|
|
// However, as of June 2020, its types are generated with tsconfig that enables
|
|
|
|
// `esModuleInterop` option.
|
|
|
|
//
|
|
|
|
// As a result, we can't depend on the package unless we enable the option
|
|
|
|
// for our codebase. Instead of doing this, we abuse "require" to import module
|
|
|
|
// without types.
|
|
|
|
const ProxyAgent = require('https-proxy-agent');
|
|
|
|
|
2020-04-01 14:42:47 -07:00
|
|
|
const unlinkAsync = util.promisify(fs.unlink.bind(fs));
|
|
|
|
const chmodAsync = util.promisify(fs.chmod.bind(fs));
|
2020-03-19 11:43:35 -07:00
|
|
|
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2020-03-19 11:43:35 -07:00
|
|
|
export type OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void;
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2020-08-10 11:40:30 -07:00
|
|
|
const CHROMIUM_MOVE_TO_AZURE_CDN_REVISION = 792639;
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2020-08-10 11:40:30 -07:00
|
|
|
function getDownloadHost(browserName: BrowserName, revision: number): string {
|
|
|
|
// Only old chromium revisions are downloaded from gbucket.
|
|
|
|
const defaultDownloadHost = browserName === 'chromium' && revision < CHROMIUM_MOVE_TO_AZURE_CDN_REVISION ? 'https://storage.googleapis.com' : 'https://playwright.azureedge.net';
|
|
|
|
|
|
|
|
const envDownloadHost: { [key: string]: string } = {
|
|
|
|
chromium: 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST',
|
|
|
|
firefox: 'PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST',
|
|
|
|
webkit: 'PLAYWRIGHT_WEBKIT_DOWNLOAD_HOST',
|
|
|
|
};
|
|
|
|
return getFromENV(envDownloadHost[browserName]) ||
|
|
|
|
getFromENV('PLAYWRIGHT_DOWNLOAD_HOST') ||
|
|
|
|
defaultDownloadHost;
|
|
|
|
}
|
2020-06-18 14:20:43 -04:00
|
|
|
|
2020-07-23 11:59:23 -07:00
|
|
|
function getDownloadUrl(browserName: BrowserName, revision: number, platform: BrowserPlatform): string | undefined {
|
2020-04-24 19:14:10 -07:00
|
|
|
if (browserName === 'chromium') {
|
2020-08-10 11:40:30 -07:00
|
|
|
return revision < CHROMIUM_MOVE_TO_AZURE_CDN_REVISION ?
|
|
|
|
new Map<BrowserPlatform, string>([
|
|
|
|
['ubuntu18.04', '%s/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip'],
|
|
|
|
['ubuntu20.04', '%s/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip'],
|
|
|
|
['mac10.13', '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip'],
|
|
|
|
['mac10.14', '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip'],
|
|
|
|
['mac10.15', '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip'],
|
|
|
|
['win32', '%s/chromium-browser-snapshots/Win/%d/chrome-win.zip'],
|
|
|
|
['win64', '%s/chromium-browser-snapshots/Win_x64/%d/chrome-win.zip'],
|
|
|
|
]).get(platform) :
|
|
|
|
new Map<BrowserPlatform, string>([
|
|
|
|
['ubuntu18.04', '%s/builds/chromium/%s/chromium-linux.zip'],
|
|
|
|
['ubuntu20.04', '%s/builds/chromium/%s/chromium-linux.zip'],
|
|
|
|
['mac10.13', '%s/builds/chromium/%s/chromium-mac.zip'],
|
|
|
|
['mac10.14', '%s/builds/chromium/%s/chromium-mac.zip'],
|
|
|
|
['mac10.15', '%s/builds/chromium/%s/chromium-mac.zip'],
|
|
|
|
['win32', '%s/builds/chromium/%s/chromium-win32.zip'],
|
|
|
|
['win64', '%s/builds/chromium/%s/chromium-win64.zip'],
|
|
|
|
]).get(platform);
|
2020-04-24 19:14:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (browserName === 'firefox') {
|
2020-07-24 00:31:18 -07:00
|
|
|
const FIREFOX_NORMALIZE_CDN_NAMES_REVISION = 1140;
|
|
|
|
return revision < FIREFOX_NORMALIZE_CDN_NAMES_REVISION ?
|
2020-07-23 11:59:23 -07:00
|
|
|
new Map<BrowserPlatform, string>([
|
2020-07-23 17:37:47 -07:00
|
|
|
['ubuntu18.04', '%s/builds/firefox/%s/firefox-linux.zip'],
|
|
|
|
['ubuntu20.04', '%s/builds/firefox/%s/firefox-linux.zip'],
|
2020-07-23 11:59:23 -07:00
|
|
|
['mac10.13', '%s/builds/firefox/%s/firefox-mac.zip'],
|
|
|
|
['mac10.14', '%s/builds/firefox/%s/firefox-mac.zip'],
|
|
|
|
['mac10.15', '%s/builds/firefox/%s/firefox-mac.zip'],
|
|
|
|
['win32', '%s/builds/firefox/%s/firefox-win32.zip'],
|
|
|
|
['win64', '%s/builds/firefox/%s/firefox-win64.zip'],
|
|
|
|
]).get(platform) :
|
|
|
|
new Map<BrowserPlatform, string>([
|
2020-07-23 17:37:47 -07:00
|
|
|
['ubuntu18.04', '%s/builds/firefox/%s/firefox-ubuntu-18.04.zip'],
|
|
|
|
['ubuntu20.04', '%s/builds/firefox/%s/firefox-ubuntu-18.04.zip'],
|
2020-07-24 00:31:18 -07:00
|
|
|
['mac10.13', '%s/builds/firefox/%s/firefox-mac-10.14.zip'],
|
|
|
|
['mac10.14', '%s/builds/firefox/%s/firefox-mac-10.14.zip'],
|
|
|
|
['mac10.15', '%s/builds/firefox/%s/firefox-mac-10.14.zip'],
|
2020-07-23 11:59:23 -07:00
|
|
|
['win32', '%s/builds/firefox/%s/firefox-win32.zip'],
|
|
|
|
['win64', '%s/builds/firefox/%s/firefox-win64.zip'],
|
|
|
|
]).get(platform);
|
2020-04-24 19:14:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (browserName === 'webkit') {
|
2020-07-24 00:31:18 -07:00
|
|
|
const WEBKIT_NORMALIZE_CDN_NAMES_REVISION = 1317;
|
|
|
|
return revision < WEBKIT_NORMALIZE_CDN_NAMES_REVISION ?
|
2020-07-23 11:59:23 -07:00
|
|
|
new Map<BrowserPlatform, string | undefined>([
|
2020-07-23 17:37:47 -07:00
|
|
|
['ubuntu18.04', '%s/builds/webkit/%s/minibrowser-gtk-wpe.zip'],
|
|
|
|
['ubuntu20.04', '%s/builds/webkit/%s/minibrowser-gtk-wpe.zip'],
|
2020-07-23 11:59:23 -07:00
|
|
|
['mac10.13', undefined],
|
|
|
|
['mac10.14', '%s/builds/webkit/%s/minibrowser-mac-10.14.zip'],
|
|
|
|
['mac10.15', '%s/builds/webkit/%s/minibrowser-mac-10.15.zip'],
|
|
|
|
['win32', '%s/builds/webkit/%s/minibrowser-win64.zip'],
|
|
|
|
['win64', '%s/builds/webkit/%s/minibrowser-win64.zip'],
|
|
|
|
]).get(platform) :
|
|
|
|
new Map<BrowserPlatform, string | undefined>([
|
2020-07-24 00:31:18 -07:00
|
|
|
['ubuntu18.04', '%s/builds/webkit/%s/webkit-ubuntu-18.04.zip'],
|
|
|
|
['ubuntu20.04', '%s/builds/webkit/%s/webkit-ubuntu-20.04.zip'],
|
2020-07-23 11:59:23 -07:00
|
|
|
['mac10.13', undefined],
|
2020-07-24 00:31:18 -07:00
|
|
|
['mac10.14', '%s/builds/webkit/%s/webkit-mac-10.14.zip'],
|
|
|
|
['mac10.15', '%s/builds/webkit/%s/webkit-mac-10.15.zip'],
|
|
|
|
['win32', '%s/builds/webkit/%s/webkit-win64.zip'],
|
|
|
|
['win64', '%s/builds/webkit/%s/webkit-win64.zip'],
|
2020-07-23 11:59:23 -07:00
|
|
|
]).get(platform);
|
2020-04-24 19:14:10 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 17:06:01 -07:00
|
|
|
function revisionURL(browser: BrowserDescriptor, platform = browserPaths.hostPlatform): string {
|
devops: encode build number together with Chromium revision (#3769)
This is an alternative approach to #3698 that was setting up a custom
mapping between chromium revisions and our mirrored builds. For example, we were
taking chromium `792639` and re-packaging it to our CDN as Chromium 1000.
One big downside of this opaque mapping was inability to quickly
understand which Chromium is mirrored to CDN.
To solve this, this patch starts treating browser revision as a fractional number,
with and integer part being a chromium revision, and fractional
part being our build number. For example, we can generate builds `792639`, `792639.1`,
`792639.2` etc, all of which will pick Chromium `792639` and re-package it to our CDN.
In the Playwright code itself, there are a handful of places that treat
browser revision as integer, exclusively to compare revision with some particular
revision numbers. This code would still work as-is, but I changed these places
to use `parseFloat` instead of `parseInt` for correctness.
2020-09-04 03:12:30 -07:00
|
|
|
const revision = parseFloat(browser.revision);
|
2020-08-10 11:40:30 -07:00
|
|
|
const serverHost = getDownloadHost(browser.name, revision);
|
|
|
|
const urlTemplate = getDownloadUrl(browser.name, revision, platform);
|
2020-04-28 17:06:01 -07:00
|
|
|
assert(urlTemplate, `ERROR: Playwright does not support ${browser.name} on ${platform}`);
|
|
|
|
return util.format(urlTemplate, serverHost, browser.revision);
|
2020-03-19 11:43:35 -07:00
|
|
|
}
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2020-07-24 16:36:00 -07:00
|
|
|
export async function downloadBrowserWithProgressBar(browsersPath: string, browser: BrowserDescriptor): Promise<boolean> {
|
|
|
|
const browserPath = browserPaths.browserDirectory(browsersPath, browser);
|
2020-04-28 17:06:01 -07:00
|
|
|
const progressBarName = `${browser.name} v${browser.revision}`;
|
2020-04-29 17:19:21 -07:00
|
|
|
if (await existsAsync(browserPath)) {
|
2020-04-24 19:14:10 -07:00
|
|
|
// Already downloaded.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let progressBar: ProgressBar;
|
|
|
|
let lastDownloadedBytes = 0;
|
|
|
|
|
|
|
|
function progress(downloadedBytes: number, totalBytes: number) {
|
|
|
|
if (!progressBar) {
|
|
|
|
progressBar = new ProgressBar(`Downloading ${progressBarName} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
|
|
|
|
complete: '=',
|
|
|
|
incomplete: ' ',
|
|
|
|
width: 20,
|
|
|
|
total: totalBytes,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
const delta = downloadedBytes - lastDownloadedBytes;
|
|
|
|
lastDownloadedBytes = downloadedBytes;
|
|
|
|
progressBar.tick(delta);
|
|
|
|
}
|
|
|
|
|
2020-04-28 17:06:01 -07:00
|
|
|
const url = revisionURL(browser);
|
2020-06-10 18:49:03 -07:00
|
|
|
const zipPath = path.join(os.tmpdir(), `playwright-download-${browser.name}-${browserPaths.hostPlatform}-${browser.revision}.zip`);
|
2020-03-19 11:43:35 -07:00
|
|
|
try {
|
|
|
|
await downloadFile(url, zipPath, progress);
|
2020-06-10 18:49:03 -07:00
|
|
|
await extract(zipPath, { dir: browserPath});
|
|
|
|
await chmodAsync(browserPaths.executablePath(browserPath, browser)!, 0o755);
|
2020-04-24 19:14:10 -07:00
|
|
|
} catch (e) {
|
|
|
|
process.exitCode = 1;
|
|
|
|
throw e;
|
2020-03-19 11:43:35 -07:00
|
|
|
} finally {
|
|
|
|
if (await existsAsync(zipPath))
|
|
|
|
await unlinkAsync(zipPath);
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
2020-04-29 17:19:21 -07:00
|
|
|
logPolitely(`${progressBarName} downloaded to ${browserPath}`);
|
2020-04-24 19:14:10 -07:00
|
|
|
return true;
|
2020-03-24 00:08:00 -07:00
|
|
|
}
|
|
|
|
|
2020-04-24 19:14:10 -07:00
|
|
|
function toMegabytes(bytes: number) {
|
|
|
|
const mb = bytes / 1024 / 1024;
|
|
|
|
return `${Math.round(mb * 10) / 10} Mb`;
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
|
2020-01-13 13:33:25 -08:00
|
|
|
function downloadFile(url: string, destinationPath: string, progressCallback: OnProgressCallback | undefined): Promise<any> {
|
|
|
|
let fulfill: () => void = () => {};
|
|
|
|
let reject: (error: any) => void = () => {};
|
2019-11-18 18:18:28 -08:00
|
|
|
let downloadedBytes = 0;
|
|
|
|
let totalBytes = 0;
|
|
|
|
|
|
|
|
const promise = new Promise((x, y) => { fulfill = x; reject = y; });
|
|
|
|
|
|
|
|
const request = httpRequest(url, 'GET', response => {
|
|
|
|
if (response.statusCode !== 200) {
|
|
|
|
const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
|
|
|
|
// consume response data to free up memory
|
|
|
|
response.resume();
|
|
|
|
reject(error);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const file = fs.createWriteStream(destinationPath);
|
|
|
|
file.on('finish', () => fulfill());
|
|
|
|
file.on('error', error => reject(error));
|
|
|
|
response.pipe(file);
|
|
|
|
totalBytes = parseInt(response.headers['content-length'], 10);
|
|
|
|
if (progressCallback)
|
|
|
|
response.on('data', onData);
|
|
|
|
});
|
2020-01-13 13:33:25 -08:00
|
|
|
request.on('error', (error: any) => reject(error));
|
2019-11-18 18:18:28 -08:00
|
|
|
return promise;
|
|
|
|
|
2020-01-13 13:33:25 -08:00
|
|
|
function onData(chunk: string) {
|
2019-11-18 18:18:28 -08:00
|
|
|
downloadedBytes += chunk.length;
|
2020-01-13 13:33:25 -08:00
|
|
|
progressCallback!(downloadedBytes, totalBytes);
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function httpRequest(url: string, method: string, response: (r: any) => void) {
|
|
|
|
let options: any = URL.parse(url);
|
|
|
|
options.method = method;
|
|
|
|
|
|
|
|
const proxyURL = getProxyForUrl(url);
|
|
|
|
if (proxyURL) {
|
|
|
|
if (url.startsWith('http:')) {
|
|
|
|
const proxy = URL.parse(proxyURL);
|
|
|
|
options = {
|
|
|
|
path: options.href,
|
|
|
|
host: proxy.hostname,
|
|
|
|
port: proxy.port,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
const parsedProxyURL: any = URL.parse(proxyURL);
|
|
|
|
parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
|
|
|
|
|
|
|
|
options.agent = new ProxyAgent(parsedProxyURL);
|
|
|
|
options.rejectUnauthorized = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-13 13:33:25 -08:00
|
|
|
const requestCallback = (res: any) => {
|
2019-11-18 18:18:28 -08:00
|
|
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
|
|
|
|
httpRequest(res.headers.location, method, response);
|
|
|
|
else
|
|
|
|
response(res);
|
|
|
|
};
|
|
|
|
const request = options.protocol === 'https:' ?
|
|
|
|
require('https').request(options, requestCallback) :
|
|
|
|
require('http').request(options, requestCallback);
|
|
|
|
request.end();
|
|
|
|
return request;
|
|
|
|
}
|
2020-08-17 16:19:21 -07:00
|
|
|
|
|
|
|
export function logPolitely(toBeLogged: string) {
|
|
|
|
const logLevel = process.env.npm_config_loglevel;
|
|
|
|
const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel || '') > -1;
|
|
|
|
|
|
|
|
if (!logLevelDisplay)
|
|
|
|
console.log(toBeLogged); // eslint-disable-line no-console
|
|
|
|
}
|