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.
|
|
|
|
*/
|
|
|
|
|
2021-02-11 06:36:15 -08:00
|
|
|
import extract from 'extract-zip';
|
|
|
|
import fs from 'fs';
|
|
|
|
import os from 'os';
|
|
|
|
import path from 'path';
|
|
|
|
import ProgressBar from 'progress';
|
2019-11-18 18:18:28 -08:00
|
|
|
import { getProxyForUrl } from 'proxy-from-env';
|
|
|
|
import * as URL from 'url';
|
2021-02-08 16:02:49 -08:00
|
|
|
import { BrowserName, Registry, hostPlatform } from '../utils/registry';
|
2021-05-18 17:38:02 -07:00
|
|
|
import { debugLogger } from '../utils/debugLogger';
|
2019-11-18 18:18:28 -08:00
|
|
|
|
2021-03-26 18:47:16 +01:00
|
|
|
// `https-proxy-agent` v5 is written in TypeScript and exposes generated types.
|
2020-06-30 17:03:01 -07:00
|
|
|
// 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-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
|
|
|
|
2021-02-08 16:02:49 -08:00
|
|
|
export async function downloadBrowserWithProgressBar(registry: Registry, browserName: BrowserName): Promise<boolean> {
|
|
|
|
const browserDirectory = registry.browserDirectory(browserName);
|
|
|
|
const progressBarName = `${browserName} v${registry.revision(browserName)}`;
|
|
|
|
if (await existsAsync(browserDirectory)) {
|
2020-04-24 19:14:10 -07:00
|
|
|
// Already downloaded.
|
2021-05-18 17:38:02 -07:00
|
|
|
debugLogger.log('install', `browser ${browserName} is already downloaded.`);
|
2020-04-24 19:14:10 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let progressBar: ProgressBar;
|
|
|
|
let lastDownloadedBytes = 0;
|
|
|
|
|
|
|
|
function progress(downloadedBytes: number, totalBytes: number) {
|
2021-05-25 07:10:45 +02:00
|
|
|
if (!process.stderr.isTTY)
|
|
|
|
return;
|
2020-04-24 19:14:10 -07:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-02-08 16:02:49 -08:00
|
|
|
const url = registry.downloadURL(browserName);
|
|
|
|
const zipPath = path.join(os.tmpdir(), `playwright-download-${browserName}-${hostPlatform}-${registry.revision(browserName)}.zip`);
|
2020-03-19 11:43:35 -07:00
|
|
|
try {
|
2021-01-28 00:37:42 -08:00
|
|
|
for (let attempt = 1, N = 3; attempt <= N; ++attempt) {
|
2021-05-18 17:38:02 -07:00
|
|
|
debugLogger.log('install', `downloading ${progressBarName} - attempt #${attempt}`);
|
2021-01-28 00:37:42 -08:00
|
|
|
const {error} = await downloadFile(url, zipPath, progress);
|
2021-05-18 17:38:02 -07:00
|
|
|
if (!error) {
|
|
|
|
debugLogger.log('install', `SUCCESS downloading ${progressBarName}`);
|
2021-01-28 00:37:42 -08:00
|
|
|
break;
|
2021-05-18 17:38:02 -07:00
|
|
|
}
|
2021-02-01 09:30:22 -08:00
|
|
|
const errorMessage = typeof error === 'object' && typeof error.message === 'string' ? error.message : '';
|
2021-05-18 17:38:02 -07:00
|
|
|
debugLogger.log('install', `attempt #${attempt} - ERROR: ${errorMessage}`);
|
2021-02-01 09:30:22 -08:00
|
|
|
if (attempt < N && (errorMessage.includes('ECONNRESET') || errorMessage.includes('ETIMEDOUT'))) {
|
2021-01-28 00:37:42 -08:00
|
|
|
// Maximum delay is 3rd retry: 1337.5ms
|
|
|
|
const millis = (Math.random() * 200) + (250 * Math.pow(1.5, attempt));
|
2021-05-18 17:38:02 -07:00
|
|
|
debugLogger.log('install', `sleeping ${millis}ms before retry...`);
|
2021-01-28 00:37:42 -08:00
|
|
|
await new Promise(c => setTimeout(c, millis));
|
|
|
|
} else {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
2021-05-18 17:38:02 -07:00
|
|
|
debugLogger.log('install', `extracting archive`);
|
|
|
|
debugLogger.log('install', `-- zip: ${zipPath}`);
|
|
|
|
debugLogger.log('install', `-- location: ${browserDirectory}`);
|
2021-02-08 16:02:49 -08:00
|
|
|
await extract(zipPath, { dir: browserDirectory});
|
2021-05-18 17:38:02 -07:00
|
|
|
const executablePath = registry.executablePath(browserName)!;
|
|
|
|
debugLogger.log('install', `fixing permissions at ${executablePath}`);
|
2021-06-03 09:55:33 -07:00
|
|
|
await fs.promises.chmod(executablePath, 0o755);
|
2020-04-24 19:14:10 -07:00
|
|
|
} catch (e) {
|
2021-05-18 17:38:02 -07:00
|
|
|
debugLogger.log('install', `FAILED installation ${progressBarName} with error: ${e}`);
|
2020-04-24 19:14:10 -07:00
|
|
|
process.exitCode = 1;
|
|
|
|
throw e;
|
2020-03-19 11:43:35 -07:00
|
|
|
} finally {
|
|
|
|
if (await existsAsync(zipPath))
|
2021-06-03 09:55:33 -07:00
|
|
|
await fs.promises.unlink(zipPath);
|
2019-11-18 18:18:28 -08:00
|
|
|
}
|
2021-02-08 16:02:49 -08:00
|
|
|
logPolitely(`${progressBarName} downloaded to ${browserDirectory}`);
|
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
|
|
|
}
|
|
|
|
|
2021-01-28 00:37:42 -08:00
|
|
|
function downloadFile(url: string, destinationPath: string, progressCallback: OnProgressCallback | undefined): Promise<{error: any}> {
|
2021-05-18 17:38:02 -07:00
|
|
|
debugLogger.log('install', `running download:`);
|
|
|
|
debugLogger.log('install', `-- from url: ${url}`);
|
|
|
|
debugLogger.log('install', `-- to location: ${destinationPath}`);
|
2021-01-28 00:37:42 -08:00
|
|
|
let fulfill: ({error}: {error: any}) => void = ({error}) => {};
|
2019-11-18 18:18:28 -08:00
|
|
|
let downloadedBytes = 0;
|
|
|
|
let totalBytes = 0;
|
|
|
|
|
2021-01-28 00:37:42 -08:00
|
|
|
const promise: Promise<{error: any}> = new Promise(x => { fulfill = x; });
|
2019-11-18 18:18:28 -08:00
|
|
|
|
|
|
|
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();
|
2021-01-28 00:37:42 -08:00
|
|
|
fulfill({error});
|
2019-11-18 18:18:28 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const file = fs.createWriteStream(destinationPath);
|
2021-01-28 00:37:42 -08:00
|
|
|
file.on('finish', () => fulfill({error: null}));
|
|
|
|
file.on('error', error => fulfill({error}));
|
2019-11-18 18:18:28 -08:00
|
|
|
response.pipe(file);
|
|
|
|
totalBytes = parseInt(response.headers['content-length'], 10);
|
2021-05-18 17:38:02 -07:00
|
|
|
debugLogger.log('install', `-- total bytes: ${totalBytes}`);
|
2019-11-18 18:18:28 -08:00
|
|
|
if (progressCallback)
|
|
|
|
response.on('data', onData);
|
|
|
|
});
|
2021-01-28 00:37:42 -08:00
|
|
|
request.on('error', (error: any) => fulfill({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
|
|
|
|
}
|