/** * 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'; import * as os from 'os'; import * as path from 'path'; import * as ProgressBar from 'progress'; import { getProxyForUrl } from 'proxy-from-env'; import * as URL from 'url'; import * as util from 'util'; import { assert, getFromENV } from '../utils/utils'; import * as browserPaths from '../utils/browserPaths'; import { BrowserName, BrowserPlatform, BrowserDescriptor } from '../utils/browserPaths'; // `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'); const unlinkAsync = util.promisify(fs.unlink.bind(fs)); const chmodAsync = util.promisify(fs.chmod.bind(fs)); const existsAsync = (path: string): Promise => new Promise(resolve => fs.stat(path, err => resolve(!err))); export type OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void; const CHROMIUM_MOVE_TO_AZURE_CDN_REVISION = 792639; 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; } function getDownloadUrl(browserName: BrowserName, revision: number, platform: BrowserPlatform): string | undefined { if (browserName === 'chromium') { return revision < CHROMIUM_MOVE_TO_AZURE_CDN_REVISION ? new Map([ ['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'], ['mac11', '%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([ ['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'], ['mac11', '%s/builds/chromium/%s/chromium-mac.zip'], ['mac11-arm64', '%s/builds/chromium/%s/chromium-mac-arm64.zip'], ['win32', '%s/builds/chromium/%s/chromium-win32.zip'], ['win64', '%s/builds/chromium/%s/chromium-win64.zip'], ]).get(platform); } if (browserName === 'firefox') { const FIREFOX_NORMALIZE_CDN_NAMES_REVISION = 1140; return revision < FIREFOX_NORMALIZE_CDN_NAMES_REVISION ? new Map([ ['ubuntu18.04', '%s/builds/firefox/%s/firefox-linux.zip'], ['ubuntu20.04', '%s/builds/firefox/%s/firefox-linux.zip'], ['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'], ['mac11', '%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([ ['ubuntu18.04', '%s/builds/firefox/%s/firefox-ubuntu-18.04.zip'], ['ubuntu20.04', '%s/builds/firefox/%s/firefox-ubuntu-18.04.zip'], ['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'], ['mac11', '%s/builds/firefox/%s/firefox-mac-10.14.zip'], ['mac11-arm64', '%s/builds/firefox/%s/firefox-mac-11.0-arm64.zip'], ['win32', '%s/builds/firefox/%s/firefox-win32.zip'], ['win64', '%s/builds/firefox/%s/firefox-win64.zip'], ]).get(platform); } if (browserName === 'webkit') { const WEBKIT_NORMALIZE_CDN_NAMES_REVISION = 1317; return revision < WEBKIT_NORMALIZE_CDN_NAMES_REVISION ? new Map([ ['ubuntu18.04', '%s/builds/webkit/%s/minibrowser-gtk-wpe.zip'], ['ubuntu20.04', '%s/builds/webkit/%s/minibrowser-gtk-wpe.zip'], ['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'], ['mac11', '%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([ ['ubuntu18.04', '%s/builds/webkit/%s/webkit-ubuntu-18.04.zip'], ['ubuntu20.04', '%s/builds/webkit/%s/webkit-ubuntu-20.04.zip'], ['mac10.13', undefined], ['mac10.14', '%s/builds/webkit/%s/webkit-mac-10.14.zip'], ['mac10.15', '%s/builds/webkit/%s/webkit-mac-10.15.zip'], ['mac11', '%s/builds/webkit/%s/webkit-mac-10.15.zip'], ['mac11-arm64', '%s/builds/webkit/%s/webkit-mac-11.0-arm64.zip'], ['win32', '%s/builds/webkit/%s/webkit-win64.zip'], ['win64', '%s/builds/webkit/%s/webkit-win64.zip'], ]).get(platform); } } function revisionURL(browser: BrowserDescriptor, platform = browserPaths.hostPlatform): string { const revision = parseInt(browser.revision, 10); const serverHost = getDownloadHost(browser.name, revision); const urlTemplate = getDownloadUrl(browser.name, revision, platform); assert(urlTemplate, `ERROR: Playwright does not support ${browser.name} on ${platform}`); return util.format(urlTemplate, serverHost, browser.revision); } export async function downloadBrowserWithProgressBar(browsersPath: string, browser: BrowserDescriptor): Promise { const browserPath = browserPaths.browserDirectory(browsersPath, browser); const progressBarName = `${browser.name} v${browser.revision}`; if (await existsAsync(browserPath)) { // 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); } const url = revisionURL(browser); const zipPath = path.join(os.tmpdir(), `playwright-download-${browser.name}-${browserPaths.hostPlatform}-${browser.revision}.zip`); try { await downloadFile(url, zipPath, progress); await extract(zipPath, { dir: browserPath}); await chmodAsync(browserPaths.executablePath(browserPath, browser)!, 0o755); } catch (e) { process.exitCode = 1; throw e; } finally { if (await existsAsync(zipPath)) await unlinkAsync(zipPath); } logPolitely(`${progressBarName} downloaded to ${browserPath}`); return true; } function toMegabytes(bytes: number) { const mb = bytes / 1024 / 1024; return `${Math.round(mb * 10) / 10} Mb`; } function downloadFile(url: string, destinationPath: string, progressCallback: OnProgressCallback | undefined): Promise { let fulfill: () => void = () => {}; let reject: (error: any) => void = () => {}; 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); }); request.on('error', (error: any) => reject(error)); return promise; function onData(chunk: string) { downloadedBytes += chunk.length; progressCallback!(downloadedBytes, totalBytes); } } 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; } } const requestCallback = (res: any) => { 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; } 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 }