2020-08-22 07:07:13 -07:00
|
|
|
/**
|
|
|
|
* 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 path from 'path';
|
|
|
|
import fs from 'fs';
|
2021-08-07 15:47:03 -07:00
|
|
|
import stream from 'stream';
|
2021-03-17 10:31:35 +08:00
|
|
|
import removeFolder from 'rimraf';
|
2020-08-28 10:51:55 -07:00
|
|
|
import * as crypto from 'crypto';
|
2021-06-03 00:06:58 +05:30
|
|
|
import os from 'os';
|
2021-09-20 13:50:26 -07:00
|
|
|
import http from 'http';
|
|
|
|
import https from 'https';
|
2022-01-14 11:46:17 +01:00
|
|
|
import { spawn, SpawnOptions, execSync } from 'child_process';
|
2021-06-07 00:23:22 -07:00
|
|
|
import { getProxyForUrl } from 'proxy-from-env';
|
|
|
|
import * as URL from 'url';
|
2022-01-14 11:46:17 +01:00
|
|
|
import { getUbuntuVersionSync, parseOSReleaseText } from './ubuntuVersion';
|
2021-09-13 14:29:44 -07:00
|
|
|
import { NameValue } from '../protocol/channels';
|
2021-10-01 19:38:41 +05:30
|
|
|
import ProgressBar from 'progress';
|
2021-06-07 00:23:22 -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');
|
|
|
|
|
|
|
|
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
|
|
|
|
2021-11-09 14:41:13 -08:00
|
|
|
export type HTTPRequestParams = {
|
2021-09-20 13:50:26 -07:00
|
|
|
url: string,
|
|
|
|
method?: string,
|
|
|
|
headers?: http.OutgoingHttpHeaders,
|
|
|
|
data?: string | Buffer,
|
2021-09-22 21:13:32 -07:00
|
|
|
timeout?: number,
|
2021-09-20 13:50:26 -07:00
|
|
|
};
|
2021-06-07 00:23:22 -07:00
|
|
|
|
2021-09-20 13:50:26 -07:00
|
|
|
function httpRequest(params: HTTPRequestParams, onResponse: (r: http.IncomingMessage) => void, onError: (error: Error) => void) {
|
|
|
|
const parsedUrl = URL.parse(params.url);
|
|
|
|
let options: https.RequestOptions = { ...parsedUrl };
|
|
|
|
options.method = params.method || 'GET';
|
|
|
|
options.headers = params.headers;
|
|
|
|
|
|
|
|
const proxyURL = getProxyForUrl(params.url);
|
2021-06-07 00:23:22 -07:00
|
|
|
if (proxyURL) {
|
2021-09-20 13:50:26 -07:00
|
|
|
if (params.url.startsWith('http:')) {
|
2021-06-07 00:23:22 -07:00
|
|
|
const proxy = URL.parse(proxyURL);
|
|
|
|
options = {
|
2021-09-20 13:50:26 -07:00
|
|
|
path: parsedUrl.href,
|
2021-06-07 00:23:22 -07:00
|
|
|
host: proxy.hostname,
|
|
|
|
port: proxy.port,
|
|
|
|
};
|
|
|
|
} else {
|
2021-09-20 13:50:26 -07:00
|
|
|
const parsedProxyURL = URL.parse(proxyURL);
|
|
|
|
(parsedProxyURL as any).secureProxy = parsedProxyURL.protocol === 'https:';
|
2021-06-07 00:23:22 -07:00
|
|
|
|
|
|
|
options.agent = new ProxyAgent(parsedProxyURL);
|
|
|
|
options.rejectUnauthorized = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-20 13:50:26 -07:00
|
|
|
const requestCallback = (res: http.IncomingMessage) => {
|
|
|
|
const statusCode = res.statusCode || 0;
|
|
|
|
if (statusCode >= 300 && statusCode < 400 && res.headers.location)
|
|
|
|
httpRequest({ ...params, url: res.headers.location }, onResponse, onError);
|
2021-06-07 00:23:22 -07:00
|
|
|
else
|
2021-09-20 13:50:26 -07:00
|
|
|
onResponse(res);
|
2021-06-07 00:23:22 -07:00
|
|
|
};
|
|
|
|
const request = options.protocol === 'https:' ?
|
2021-09-20 13:50:26 -07:00
|
|
|
https.request(options, requestCallback) :
|
|
|
|
http.request(options, requestCallback);
|
|
|
|
request.on('error', onError);
|
2021-09-22 21:13:32 -07:00
|
|
|
if (params.timeout !== undefined) {
|
|
|
|
const rejectOnTimeout = () => {
|
|
|
|
onError(new Error(`Request to ${params.url} timed out after ${params.timeout}ms`));
|
|
|
|
request.abort();
|
|
|
|
};
|
|
|
|
if (params.timeout <= 0) {
|
|
|
|
rejectOnTimeout();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
request.setTimeout(params.timeout, rejectOnTimeout);
|
|
|
|
}
|
2021-09-20 13:50:26 -07:00
|
|
|
request.end(params.data);
|
2021-06-07 00:23:22 -07:00
|
|
|
}
|
|
|
|
|
2021-11-09 14:41:13 -08:00
|
|
|
export function fetchData(params: HTTPRequestParams, onError?: (params: HTTPRequestParams, response: http.IncomingMessage) => Promise<Error>): Promise<string> {
|
2021-06-07 00:23:22 -07:00
|
|
|
return new Promise((resolve, reject) => {
|
2021-09-22 21:13:32 -07:00
|
|
|
httpRequest(params, async response => {
|
2021-06-07 00:23:22 -07:00
|
|
|
if (response.statusCode !== 200) {
|
2021-11-09 14:41:13 -08:00
|
|
|
const error = onError ? await onError(params, response) : new Error(`fetch failed: server returned code ${response.statusCode}. URL: ${params.url}`);
|
2021-09-22 21:13:32 -07:00
|
|
|
reject(error);
|
2021-06-07 00:23:22 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
let body = '';
|
|
|
|
response.on('data', (chunk: string) => body += chunk);
|
|
|
|
response.on('error', (error: any) => reject(error));
|
|
|
|
response.on('end', () => resolve(body));
|
2021-09-20 13:50:26 -07:00
|
|
|
}, reject);
|
2021-06-07 00:23:22 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
type OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void;
|
|
|
|
type DownloadFileLogger = (message: string) => void;
|
2022-01-14 11:46:17 +01:00
|
|
|
type DownloadFileOptions = {
|
|
|
|
progressCallback?: OnProgressCallback,
|
|
|
|
log?: DownloadFileLogger,
|
|
|
|
userAgent?: string
|
|
|
|
};
|
2021-06-07 00:23:22 -07:00
|
|
|
|
2022-01-14 11:46:17 +01:00
|
|
|
function downloadFile(url: string, destinationPath: string, options: DownloadFileOptions = {}): Promise<{error: any}> {
|
2021-06-07 00:23:22 -07:00
|
|
|
const {
|
|
|
|
progressCallback,
|
|
|
|
log = () => {},
|
|
|
|
} = options;
|
|
|
|
log(`running download:`);
|
|
|
|
log(`-- from url: ${url}`);
|
|
|
|
log(`-- to location: ${destinationPath}`);
|
2021-09-27 18:58:08 +02:00
|
|
|
let fulfill: ({ error }: {error: any}) => void = ({ error }) => {};
|
2021-06-07 00:23:22 -07:00
|
|
|
let downloadedBytes = 0;
|
|
|
|
let totalBytes = 0;
|
|
|
|
|
|
|
|
const promise: Promise<{error: any}> = new Promise(x => { fulfill = x; });
|
|
|
|
|
2022-01-14 11:46:17 +01:00
|
|
|
httpRequest({
|
|
|
|
url,
|
|
|
|
headers: options.userAgent ? {
|
|
|
|
'User-Agent': options.userAgent,
|
|
|
|
} : undefined,
|
|
|
|
}, response => {
|
2021-06-07 00:23:22 -07:00
|
|
|
log(`-- response status code: ${response.statusCode}`);
|
|
|
|
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-09-27 18:58:08 +02:00
|
|
|
fulfill({ error });
|
2021-06-07 00:23:22 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const file = fs.createWriteStream(destinationPath);
|
2021-09-27 18:58:08 +02:00
|
|
|
file.on('finish', () => fulfill({ error: null }));
|
|
|
|
file.on('error', error => fulfill({ error }));
|
2021-06-07 00:23:22 -07:00
|
|
|
response.pipe(file);
|
2021-09-20 13:50:26 -07:00
|
|
|
totalBytes = parseInt(response.headers['content-length'] || '0', 10);
|
2021-06-07 00:23:22 -07:00
|
|
|
log(`-- total bytes: ${totalBytes}`);
|
|
|
|
if (progressCallback)
|
|
|
|
response.on('data', onData);
|
2021-09-27 18:58:08 +02:00
|
|
|
}, (error: any) => fulfill({ error }));
|
2021-06-07 00:23:22 -07:00
|
|
|
return promise;
|
|
|
|
|
|
|
|
function onData(chunk: string) {
|
|
|
|
downloadedBytes += chunk.length;
|
|
|
|
progressCallback!(downloadedBytes, totalBytes);
|
|
|
|
}
|
|
|
|
}
|
2020-08-22 07:07:13 -07:00
|
|
|
|
2022-01-14 11:46:17 +01:00
|
|
|
type DownloadOptions = {
|
|
|
|
progressBarName?: string,
|
|
|
|
retryCount?: number
|
|
|
|
log?: DownloadFileLogger
|
|
|
|
userAgent?: string
|
|
|
|
};
|
|
|
|
|
2021-10-01 19:38:41 +05:30
|
|
|
export async function download(
|
|
|
|
url: string,
|
|
|
|
destination: string,
|
2022-01-14 11:46:17 +01:00
|
|
|
options: DownloadOptions = {}
|
2021-10-01 19:38:41 +05:30
|
|
|
) {
|
2022-01-14 11:46:17 +01:00
|
|
|
const { progressBarName = 'file', retryCount = 3, log = () => {}, userAgent } = options;
|
2021-10-01 19:38:41 +05:30
|
|
|
for (let attempt = 1; attempt <= retryCount; ++attempt) {
|
|
|
|
log(
|
|
|
|
`downloading ${progressBarName} - attempt #${attempt}`
|
|
|
|
);
|
|
|
|
const { error } = await downloadFile(url, destination, {
|
|
|
|
progressCallback: getDownloadProgress(progressBarName),
|
|
|
|
log,
|
2022-01-14 11:46:17 +01:00
|
|
|
userAgent,
|
2021-10-01 19:38:41 +05:30
|
|
|
});
|
|
|
|
if (!error) {
|
|
|
|
log(`SUCCESS downloading ${progressBarName}`);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
const errorMessage = error?.message || '';
|
|
|
|
log(`attempt #${attempt} - ERROR: ${errorMessage}`);
|
|
|
|
if (
|
|
|
|
attempt < retryCount &&
|
|
|
|
(errorMessage.includes('ECONNRESET') ||
|
|
|
|
errorMessage.includes('ETIMEDOUT'))
|
|
|
|
) {
|
|
|
|
// Maximum default delay is 3rd retry: 1337.5ms
|
|
|
|
const millis = Math.random() * 200 + 250 * Math.pow(1.5, attempt);
|
|
|
|
log(`sleeping ${millis}ms before retry...`);
|
|
|
|
await new Promise(c => setTimeout(c, millis));
|
|
|
|
} else {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getDownloadProgress(progressBarName: string): OnProgressCallback {
|
|
|
|
let progressBar: ProgressBar;
|
|
|
|
let lastDownloadedBytes = 0;
|
|
|
|
|
|
|
|
return (downloadedBytes: number, totalBytes: number) => {
|
|
|
|
if (!process.stderr.isTTY)
|
|
|
|
return;
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function toMegabytes(bytes: number) {
|
|
|
|
const mb = bytes / 1024 / 1024;
|
|
|
|
return `${Math.round(mb * 10) / 10} Mb`;
|
|
|
|
}
|
|
|
|
|
2021-10-01 19:40:47 -07:00
|
|
|
export function spawnAsync(cmd: string, args: string[], options: SpawnOptions = {}): Promise<{stdout: string, stderr: string, code: number | null, error?: Error}> {
|
2022-01-19 10:38:51 -08:00
|
|
|
const process = spawn(cmd, args, Object.assign({ windowsHide: true }, options));
|
2021-06-01 18:26:12 -07:00
|
|
|
|
|
|
|
return new Promise(resolve => {
|
|
|
|
let stdout = '';
|
|
|
|
let stderr = '';
|
|
|
|
if (process.stdout)
|
|
|
|
process.stdout.on('data', data => stdout += data);
|
|
|
|
if (process.stderr)
|
|
|
|
process.stderr.on('data', data => stderr += data);
|
2021-09-27 18:58:08 +02:00
|
|
|
process.on('close', code => resolve({ stdout, stderr, code }));
|
|
|
|
process.on('error', error => resolve({ stdout, stderr, code: 0, error }));
|
2021-06-01 18:26:12 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:07:13 -07:00
|
|
|
// See https://joel.tools/microtasks/
|
|
|
|
export function makeWaitForNextTask() {
|
2021-03-18 09:29:37 -07:00
|
|
|
// As of Mar 2021, Electorn v12 doesn't create new task with `setImmediate` despite
|
|
|
|
// using Node 14 internally, so we fallback to `setTimeout(0)` instead.
|
|
|
|
// @see https://github.com/electron/electron/issues/28261
|
2021-03-22 17:39:03 -07:00
|
|
|
if ((process.versions as any).electron)
|
2021-03-18 09:29:37 -07:00
|
|
|
return (callback: () => void) => setTimeout(callback, 0);
|
2020-08-22 07:07:13 -07:00
|
|
|
if (parseInt(process.versions.node, 10) >= 11)
|
|
|
|
return setImmediate;
|
|
|
|
|
|
|
|
// Unlike Node 11, Node 10 and less have a bug with Task and MicroTask execution order:
|
|
|
|
// - https://github.com/nodejs/node/issues/22257
|
|
|
|
//
|
|
|
|
// So we can't simply run setImmediate to dispatch code in a following task.
|
|
|
|
// However, we can run setImmediate from-inside setImmediate to make sure we're getting
|
|
|
|
// in the following task.
|
|
|
|
|
|
|
|
let spinning = false;
|
|
|
|
const callbacks: (() => void)[] = [];
|
|
|
|
const loop = () => {
|
|
|
|
const callback = callbacks.shift();
|
|
|
|
if (!callback) {
|
|
|
|
spinning = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setImmediate(loop);
|
|
|
|
// Make sure to call callback() as the last thing since it's
|
|
|
|
// untrusted code that might throw.
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
return (callback: () => void) => {
|
|
|
|
callbacks.push(callback);
|
|
|
|
if (!spinning) {
|
|
|
|
spinning = true;
|
|
|
|
setImmediate(loop);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function assert(value: any, message?: string): asserts value {
|
|
|
|
if (!value)
|
2021-08-04 17:26:07 +02:00
|
|
|
throw new Error(message || 'Assertion error');
|
2020-08-22 07:07:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export function debugAssert(value: any, message?: string): asserts value {
|
2020-09-06 21:36:22 -07:00
|
|
|
if (isUnderTest() && !value)
|
2020-08-22 07:07:13 -07:00
|
|
|
throw new Error(message);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isString(obj: any): obj is string {
|
|
|
|
return typeof obj === 'string' || obj instanceof String;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isRegExp(obj: any): obj is RegExp {
|
|
|
|
return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]';
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isObject(obj: any): obj is NonNullable<object> {
|
|
|
|
return typeof obj === 'object' && obj !== null;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isError(obj: any): obj is Error {
|
|
|
|
return obj instanceof Error || (obj && obj.__proto__ && obj.__proto__.name === 'Error');
|
|
|
|
}
|
|
|
|
|
2021-04-20 15:58:34 -07:00
|
|
|
const debugEnv = getFromENV('PWDEBUG') || '';
|
|
|
|
export function debugMode() {
|
|
|
|
if (debugEnv === 'console')
|
|
|
|
return 'console';
|
|
|
|
return debugEnv ? 'inspector' : '';
|
2020-08-22 07:07:13 -07:00
|
|
|
}
|
|
|
|
|
2020-09-06 21:36:22 -07:00
|
|
|
let _isUnderTest = false;
|
|
|
|
export function setUnderTest() {
|
|
|
|
_isUnderTest = true;
|
2020-08-22 07:07:13 -07:00
|
|
|
}
|
2020-09-06 21:36:22 -07:00
|
|
|
export function isUnderTest(): boolean {
|
|
|
|
return _isUnderTest;
|
2020-08-22 07:07:13 -07:00
|
|
|
}
|
|
|
|
|
2020-10-23 12:44:12 -07:00
|
|
|
export function getFromENV(name: string): string | undefined {
|
2020-08-22 07:07:13 -07:00
|
|
|
let value = process.env[name];
|
2020-10-23 12:44:12 -07:00
|
|
|
value = value === undefined ? process.env[`npm_config_${name.toLowerCase()}`] : value;
|
|
|
|
value = value === undefined ? process.env[`npm_package_config_${name.toLowerCase()}`] : value;
|
2020-08-22 07:07:13 -07:00
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2020-10-23 12:44:12 -07:00
|
|
|
export function getAsBooleanFromENV(name: string): boolean {
|
|
|
|
const value = getFromENV(name);
|
|
|
|
return !!value && value !== 'false' && value !== '0';
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:07:13 -07:00
|
|
|
export async function mkdirIfNeeded(filePath: string) {
|
|
|
|
// This will harmlessly throw on windows if the dirname is the root directory.
|
2021-09-27 18:58:08 +02:00
|
|
|
await fs.promises.mkdir(path.dirname(filePath), { recursive: true }).catch(() => {});
|
2020-08-22 07:07:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
type HeadersArray = { name: string, value: string }[];
|
|
|
|
type HeadersObject = { [key: string]: string };
|
|
|
|
|
2021-09-02 20:48:23 -07:00
|
|
|
export function headersObjectToArray(headers: HeadersObject, separator?: string, setCookieSeparator?: string): HeadersArray {
|
|
|
|
if (!setCookieSeparator)
|
|
|
|
setCookieSeparator = separator;
|
2020-08-22 07:07:13 -07:00
|
|
|
const result: HeadersArray = [];
|
|
|
|
for (const name in headers) {
|
2021-09-02 20:48:23 -07:00
|
|
|
const values = headers[name];
|
|
|
|
if (separator) {
|
|
|
|
const sep = name.toLowerCase() === 'set-cookie' ? setCookieSeparator : separator;
|
|
|
|
for (const value of values.split(sep!))
|
|
|
|
result.push({ name, value: value.trim() });
|
|
|
|
} else {
|
|
|
|
result.push({ name, value: values });
|
|
|
|
}
|
2020-08-22 07:07:13 -07:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function headersArrayToObject(headers: HeadersArray, lowerCase: boolean): HeadersObject {
|
|
|
|
const result: HeadersObject = {};
|
|
|
|
for (const { name, value } of headers)
|
|
|
|
result[lowerCase ? name.toLowerCase() : name] = value;
|
|
|
|
return result;
|
|
|
|
}
|
2020-08-28 10:51:55 -07:00
|
|
|
|
|
|
|
export function monotonicTime(): number {
|
|
|
|
const [seconds, nanoseconds] = process.hrtime();
|
2020-10-21 23:25:57 -07:00
|
|
|
return seconds * 1000 + (nanoseconds / 1000 | 0) / 1000;
|
2020-08-28 10:51:55 -07:00
|
|
|
}
|
|
|
|
|
2021-10-01 12:11:33 -07:00
|
|
|
export function objectToArray(map?: { [key: string]: any }): NameValue[] | undefined {
|
2021-09-13 14:29:44 -07:00
|
|
|
if (!map)
|
|
|
|
return undefined;
|
|
|
|
const result = [];
|
|
|
|
for (const [name, value] of Object.entries(map))
|
2021-10-01 12:11:33 -07:00
|
|
|
result.push({ name, value: String(value) });
|
2021-09-13 14:29:44 -07:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function arrayToObject(array?: NameValue[]): { [key: string]: string } | undefined {
|
|
|
|
if (!array)
|
|
|
|
return undefined;
|
|
|
|
const result: { [key: string]: string } = {};
|
2021-09-27 18:58:08 +02:00
|
|
|
for (const { name, value } of array)
|
2021-09-13 14:29:44 -07:00
|
|
|
result[name] = value;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-02-08 16:02:49 -08:00
|
|
|
export function calculateSha1(buffer: Buffer | string): string {
|
2020-08-28 10:51:55 -07:00
|
|
|
const hash = crypto.createHash('sha1');
|
|
|
|
hash.update(buffer);
|
|
|
|
return hash.digest('hex');
|
|
|
|
}
|
|
|
|
|
|
|
|
export function createGuid(): string {
|
|
|
|
return crypto.randomBytes(16).toString('hex');
|
|
|
|
}
|
2021-03-17 10:31:35 +08:00
|
|
|
|
2021-10-11 10:52:17 -04:00
|
|
|
export async function removeFolders(dirs: string[]): Promise<Array<Error|null|undefined>> {
|
2021-05-26 10:49:38 -07:00
|
|
|
return await Promise.all(dirs.map((dir: string) => {
|
2021-10-11 10:52:17 -04:00
|
|
|
return new Promise<Error|null|undefined>(fulfill => {
|
2021-03-17 10:31:35 +08:00
|
|
|
removeFolder(dir, { maxBusyTries: 10 }, error => {
|
2021-10-01 11:15:44 -05:00
|
|
|
fulfill(error ?? undefined);
|
2021-03-17 10:31:35 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
}
|
2021-03-18 10:19:44 +08:00
|
|
|
|
|
|
|
export function canAccessFile(file: string) {
|
|
|
|
if (!file)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
try {
|
|
|
|
fs.accessSync(file);
|
|
|
|
return true;
|
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2021-05-25 17:11:32 +02:00
|
|
|
|
2022-01-14 11:46:17 +01:00
|
|
|
let cachedUserAgent: string | undefined;
|
|
|
|
export function getUserAgent(): string {
|
|
|
|
if (cachedUserAgent)
|
|
|
|
return cachedUserAgent;
|
|
|
|
try {
|
|
|
|
cachedUserAgent = determineUserAgent();
|
|
|
|
} catch (e) {
|
|
|
|
cachedUserAgent = 'Playwright/unknown';
|
|
|
|
}
|
|
|
|
return cachedUserAgent;
|
|
|
|
}
|
|
|
|
|
|
|
|
function determineUserAgent(): string {
|
|
|
|
let osIdentifier = 'unknown';
|
|
|
|
let osVersion = 'unknown';
|
|
|
|
if (process.platform === 'win32') {
|
|
|
|
const version = os.release().split('.');
|
|
|
|
osIdentifier = 'windows';
|
|
|
|
osVersion = `${version[0]}.${version[1]}`;
|
|
|
|
} else if (process.platform === 'darwin') {
|
|
|
|
const version = execSync('sw_vers -productVersion').toString().trim().split('.');
|
|
|
|
osIdentifier = 'macOS';
|
|
|
|
osVersion = `${version[0]}.${version[1]}`;
|
|
|
|
} else if (process.platform === 'linux') {
|
|
|
|
try {
|
|
|
|
// List of /etc/os-release values for different distributions could be
|
|
|
|
// found here: https://gist.github.com/aslushnikov/8ceddb8288e4cf9db3039c02e0f4fb75
|
|
|
|
const osReleaseText = fs.readFileSync('/etc/os-release', 'utf8');
|
|
|
|
const fields = parseOSReleaseText(osReleaseText);
|
|
|
|
osIdentifier = fields.get('id') || 'unknown';
|
|
|
|
osVersion = fields.get('version_id') || 'unknown';
|
|
|
|
} catch (e) {
|
|
|
|
// Linux distribution without /etc/os-release.
|
|
|
|
// Default to linux/unknown.
|
|
|
|
osIdentifier = 'linux';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let langName = 'unknown';
|
|
|
|
let langVersion = 'unknown';
|
|
|
|
if (!process.env.PW_CLI_TARGET_LANG) {
|
|
|
|
langName = 'node';
|
|
|
|
langVersion = process.version.substring(1).split('.').slice(0, 2).join('.');
|
|
|
|
} else if (['node', 'python', 'java', 'csharp'].includes(process.env.PW_CLI_TARGET_LANG)) {
|
|
|
|
langName = process.env.PW_CLI_TARGET_LANG;
|
|
|
|
langVersion = process.env.PW_CLI_TARGET_LANG_VERSION ?? 'unknown';
|
|
|
|
}
|
|
|
|
|
|
|
|
return `Playwright/${getPlaywrightVersion()} (${os.arch()}; ${osIdentifier} ${osVersion}) ${langName}/${langVersion}`;
|
feat: introduce experimental general-purpose grid (#8941)
This patch adds a general-purpose grid framework to parallelize
Playwright across multiple agents.
This patch adds two CLI commands to manage grid:
- `npx playwright experimental-grid-server` - to launch grid
- `npx playwrigth experimental-grid-agent` - to launch agent in a host
environment.
Grid server accepts an `--agent-factory` argument. A simple
`factory.js` might look like this:
```js
const child_process = require('child_process');
module.exports = {
name: 'My Simple Factory',
capacity: Infinity, // How many workers launch per agent
timeout: 10_000, // 10 seconds timeout to create agent
launch: ({agentId, gridURL, playwrightVersion}) => child_process.spawn(`npx`, [
'playwright'
'experimental-grid-agent',
'--grid-url', gridURL,
'--agent-id', agentId,
], {
cwd: __dirname,
shell: true,
stdio: 'inherit',
}),
};
```
With this `factory.js`, grid server could be launched like this:
```bash
npx playwright experimental-grid-server --factory=./factory.js
```
Once launched, it could be used with Playwright Test using env variable:
```bash
PW_GRID=http://localhost:3000 npx playwright test
```
2021-09-16 01:20:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export function getPlaywrightVersion(majorMinorOnly = false) {
|
2021-06-03 00:06:58 +05:30
|
|
|
const packageJson = require('./../../package.json');
|
feat: introduce experimental general-purpose grid (#8941)
This patch adds a general-purpose grid framework to parallelize
Playwright across multiple agents.
This patch adds two CLI commands to manage grid:
- `npx playwright experimental-grid-server` - to launch grid
- `npx playwrigth experimental-grid-agent` - to launch agent in a host
environment.
Grid server accepts an `--agent-factory` argument. A simple
`factory.js` might look like this:
```js
const child_process = require('child_process');
module.exports = {
name: 'My Simple Factory',
capacity: Infinity, // How many workers launch per agent
timeout: 10_000, // 10 seconds timeout to create agent
launch: ({agentId, gridURL, playwrightVersion}) => child_process.spawn(`npx`, [
'playwright'
'experimental-grid-agent',
'--grid-url', gridURL,
'--agent-id', agentId,
], {
cwd: __dirname,
shell: true,
stdio: 'inherit',
}),
};
```
With this `factory.js`, grid server could be launched like this:
```bash
npx playwright experimental-grid-server --factory=./factory.js
```
Once launched, it could be used with Playwright Test using env variable:
```bash
PW_GRID=http://localhost:3000 npx playwright test
```
2021-09-16 01:20:36 -07:00
|
|
|
return majorMinorOnly ? packageJson.version.split('.').slice(0, 2).join('.') : packageJson.version;
|
2021-06-03 00:06:58 +05:30
|
|
|
}
|
2021-07-06 21:16:37 +02:00
|
|
|
|
|
|
|
export function constructURLBasedOnBaseURL(baseURL: string | undefined, givenURL: string): string {
|
|
|
|
try {
|
|
|
|
return (new URL.URL(givenURL, baseURL)).toString();
|
|
|
|
} catch (e) {
|
|
|
|
return givenURL;
|
|
|
|
}
|
|
|
|
}
|
2021-07-09 16:10:23 -07:00
|
|
|
|
2021-12-29 21:40:45 -07:00
|
|
|
export type HostPlatform = 'win64' |
|
|
|
|
'mac10.13' |
|
|
|
|
'mac10.14' |
|
|
|
|
'mac10.15' |
|
|
|
|
'mac11' | 'mac11-arm64' |
|
|
|
|
'mac12' | 'mac12-arm64' |
|
|
|
|
'ubuntu18.04' | 'ubuntu18.04-arm64' |
|
|
|
|
'ubuntu20.04' | 'ubuntu20.04-arm64';
|
|
|
|
|
2021-07-09 16:10:23 -07:00
|
|
|
export const hostPlatform = ((): HostPlatform => {
|
|
|
|
const platform = os.platform();
|
|
|
|
if (platform === 'darwin') {
|
|
|
|
const ver = os.release().split('.').map((a: string) => parseInt(a, 10));
|
|
|
|
let macVersion = '';
|
|
|
|
if (ver[0] < 18) {
|
|
|
|
// Everything before 10.14 is considered 10.13.
|
|
|
|
macVersion = 'mac10.13';
|
|
|
|
} else if (ver[0] === 18) {
|
|
|
|
macVersion = 'mac10.14';
|
|
|
|
} else if (ver[0] === 19) {
|
|
|
|
macVersion = 'mac10.15';
|
|
|
|
} else {
|
|
|
|
// ver[0] >= 20
|
2021-12-29 21:40:45 -07:00
|
|
|
const LAST_STABLE_MAC_MAJOR_VERSION = 12;
|
2021-07-09 16:10:23 -07:00
|
|
|
// Best-effort support for MacOS beta versions.
|
|
|
|
macVersion = 'mac' + Math.min(ver[0] - 9, LAST_STABLE_MAC_MAJOR_VERSION);
|
|
|
|
// BigSur is the first version that might run on Apple Silicon.
|
|
|
|
if (os.cpus().some(cpu => cpu.model.includes('Apple')))
|
|
|
|
macVersion += '-arm64';
|
|
|
|
}
|
|
|
|
return macVersion as HostPlatform;
|
|
|
|
}
|
|
|
|
if (platform === 'linux') {
|
|
|
|
const ubuntuVersion = getUbuntuVersionSync();
|
2021-11-02 16:58:22 -07:00
|
|
|
const archSuffix = os.arch() === 'arm64' ? '-arm64' : '';
|
2021-07-09 16:10:23 -07:00
|
|
|
if (parseInt(ubuntuVersion, 10) <= 19)
|
2021-11-02 16:58:22 -07:00
|
|
|
return ('ubuntu18.04' + archSuffix) as HostPlatform;
|
|
|
|
return ('ubuntu20.04' + archSuffix) as HostPlatform;
|
2021-07-09 16:10:23 -07:00
|
|
|
}
|
|
|
|
if (platform === 'win32')
|
2021-08-19 16:09:04 +03:00
|
|
|
return 'win64';
|
2021-07-09 16:10:23 -07:00
|
|
|
return platform as HostPlatform;
|
|
|
|
})();
|
2021-07-16 16:00:27 -08:00
|
|
|
|
|
|
|
export function wrapInASCIIBox(text: string, padding = 0): string {
|
|
|
|
const lines = text.split('\n');
|
|
|
|
const maxLength = Math.max(...lines.map(line => line.length));
|
|
|
|
return [
|
|
|
|
'╔' + '═'.repeat(maxLength + padding * 2) + '╗',
|
|
|
|
...lines.map(line => '║' + ' '.repeat(padding) + line + ' '.repeat(maxLength - line.length + padding) + '║'),
|
|
|
|
'╚' + '═'.repeat(maxLength + padding * 2) + '╝',
|
|
|
|
].join('\n');
|
|
|
|
}
|
2021-09-16 17:48:43 -07:00
|
|
|
|
|
|
|
export function isFilePayload(value: any): boolean {
|
|
|
|
return typeof value === 'object' && value['name'] && value['mimeType'] && value['buffer'];
|
|
|
|
}
|
2021-09-22 21:13:32 -07:00
|
|
|
|
|
|
|
export function streamToString(stream: stream.Readable): Promise<string> {
|
|
|
|
return new Promise<string>((resolve, reject) => {
|
|
|
|
const chunks: Buffer[] = [];
|
|
|
|
stream.on('data', chunk => chunks.push(Buffer.from(chunk)));
|
|
|
|
stream.on('error', reject);
|
|
|
|
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
|
|
});
|
|
|
|
}
|
2021-12-06 14:49:22 -08:00
|
|
|
|
|
|
|
export async function transformCommandsForRoot(commands: string[]): Promise<{ command: string, args: string[], elevatedPermissions: boolean}> {
|
|
|
|
const isRoot = process.getuid() === 0;
|
|
|
|
if (isRoot)
|
|
|
|
return { command: 'sh', args: ['-c', `${commands.join('&& ')}`], elevatedPermissions: false };
|
|
|
|
const sudoExists = await spawnAsync('which', ['sudo']);
|
|
|
|
if (sudoExists.code === 0)
|
|
|
|
return { command: 'sudo', args: ['--', 'sh', '-c', `${commands.join('&& ')}`], elevatedPermissions: true };
|
|
|
|
return { command: 'su', args: ['root', '-c', `${commands.join('&& ')}`], elevatedPermissions: true };
|
|
|
|
}
|