appium-chromedriver
Version:
Node.js wrapper around chromedriver.
390 lines • 18.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChromedriverStorageClient = void 0;
const utils_1 = require("../utils");
const node_path_1 = __importDefault(require("node:path"));
const asyncbox_1 = require("asyncbox");
const support_1 = require("@appium/support");
const constants_1 = require("../constants");
const googleapis_1 = require("./googleapis");
const chromelabs_1 = require("./chromelabs");
const compare_versions_1 = require("compare-versions");
const semver = __importStar(require("semver"));
const MAX_PARALLEL_DOWNLOADS = 5;
const STORAGE_INFOS = [
{
url: constants_1.GOOGLEAPIS_CDN,
accept: 'application/xml',
},
{
url: `${constants_1.CHROMELABS_URL}/chrome-for-testing/known-good-versions-with-downloads.json`,
accept: 'application/json',
},
];
const CHROME_FOR_TESTING_LAST_GOOD_VERSIONS = `${constants_1.CHROMELABS_URL}/chrome-for-testing/last-known-good-versions.json`;
const log = support_1.logger.getLogger('ChromedriverStorageClient');
class ChromedriverStorageClient {
chromedriverDir;
timeout;
mapping;
constructor(args = {}) {
const { chromedriverDir = (0, utils_1.getChromedriverDir)(), timeout = constants_1.STORAGE_REQ_TIMEOUT_MS } = args;
this.chromedriverDir = chromedriverDir;
this.timeout = timeout;
this.mapping = {};
}
/**
* Retrieves chromedriver mapping from the storage
*
* @param shouldParseNotes [true] - if set to `true`
* then additional chromedrivers info is going to be retrieved and
* parsed from release notes
* @returns Promise<ChromedriverDetailsMapping>
*/
async retrieveMapping(shouldParseNotes = true) {
const retrieveResponseSafely = async ({ url, accept, }) => {
try {
return await (0, utils_1.retrieveData)(url, {
'user-agent': constants_1.USER_AGENT,
accept: `${accept}, */*`,
}, { timeout: this.timeout });
}
catch (e) {
const err = e;
log.debug(err.stack);
log.warn(`Cannot retrieve Chromedrivers info from ${url}. ` +
`Make sure this URL is accessible from your network. ` +
`Original error: ${err.message}`);
}
};
const [xmlStr, jsonStr] = await Promise.all(STORAGE_INFOS.map(retrieveResponseSafely));
// Apply the best effort approach and fetch the mapping from at least one server if possible.
// We'll fail later anyway if the target chromedriver version is not there.
if (!xmlStr && !jsonStr) {
throw new Error(`Cannot retrieve the information about available Chromedrivers from ` +
`${STORAGE_INFOS.map(({ url }) => url)}. Please make sure these URLs are available ` +
`within your local network, check Appium server logs and/or ` +
`consult the driver troubleshooting guide.`);
}
this.mapping = xmlStr ? await (0, googleapis_1.parseGoogleapiStorageXml)(xmlStr, shouldParseNotes) : {};
if (jsonStr) {
Object.assign(this.mapping, (0, chromelabs_1.parseKnownGoodVersionsWithDownloadsJson)(jsonStr));
}
return this.mapping;
}
/**
* Retrieves chromedrivers from the remote storage to the local file system
*
* @param opts - Synchronization options (versions, minBrowserVersion, osInfo)
* @throws {Error} if there was a problem while retrieving the drivers
* @returns The list of successfully synchronized driver keys
*/
async syncDrivers(opts = {}) {
if (support_1.util.isEmpty(this.mapping)) {
await this.retrieveMapping(!!opts.minBrowserVersion);
}
if (support_1.util.isEmpty(this.mapping)) {
throw new Error('Cannot retrieve chromedrivers mapping from Google storage');
}
const driversToSync = this.selectMatchingDrivers(opts.osInfo ?? (await (0, utils_1.getOsInfo)()), opts);
if (support_1.util.isEmpty(driversToSync)) {
log.debug(`There are no drivers to sync. Exiting`);
return [];
}
log.debug(`Got ${support_1.util.pluralize('driver', driversToSync.length, true)} to sync: ` +
JSON.stringify(driversToSync, null, 2));
const synchronizedDrivers = [];
const archivesRoot = await support_1.tempDir.openDir();
try {
await (0, asyncbox_1.asyncmap)([...driversToSync.entries()], async ([idx, driverKey]) => {
if (await this.retrieveDriver(idx, driverKey, archivesRoot, !support_1.util.isEmpty(opts))) {
synchronizedDrivers.push(driverKey);
}
}, { concurrency: MAX_PARALLEL_DOWNLOADS });
}
finally {
await support_1.fs.rimraf(archivesRoot);
}
if (!support_1.util.isEmpty(synchronizedDrivers)) {
log.info(`Successfully synchronized ` +
`${support_1.util.pluralize('chromedriver', synchronizedDrivers.length, true)}`);
}
else {
log.info(`No chromedrivers were synchronized`);
}
return synchronizedDrivers;
}
/**
* Returns the latest chromedriver version for Chrome for Testing
*
* @returns The latest stable chromedriver version string
* @throws {Error} if the version cannot be fetched from the remote API
*/
async getLatestKnownGoodVersion() {
let jsonStr;
try {
jsonStr = await (0, utils_1.retrieveData)(CHROME_FOR_TESTING_LAST_GOOD_VERSIONS, {
'user-agent': constants_1.USER_AGENT,
accept: `application/json, */*`,
}, { timeout: constants_1.STORAGE_REQ_TIMEOUT_MS });
}
catch (e) {
const detail = e instanceof Error ? e.message : String(e);
throw new Error(`Cannot fetch the latest Chromedriver version. ` +
`Make sure you can access ${CHROME_FOR_TESTING_LAST_GOOD_VERSIONS} from your machine or provide a mirror by setting ` +
`a custom value to CHROMELABS_URL environment variable. Original error: ${detail}`, { cause: e });
}
return (0, chromelabs_1.parseLatestKnownGoodVersionsJson)(jsonStr);
}
/**
* Filters `this.mapping` to only select matching chromedriver entries
* by operating system information and/or additional synchronization options
*
* @param osInfo - Operating system information to match against
* @param opts - Synchronization options (versions, minBrowserVersion)
* @returns The list of filtered chromedriver entry names (version/archive name)
*/
selectMatchingDrivers(osInfo, opts = {}) {
const { minBrowserVersion, versions = [] } = opts;
let driversToSync = Object.keys(this.mapping);
if (!support_1.util.isEmpty(versions)) {
// Handle only selected versions if requested
log.debug(`Selecting chromedrivers whose versions match to ${versions}`);
driversToSync = driversToSync.filter((cdName) => versions.includes(`${this.mapping[cdName].version}`));
log.debug(`Got ${support_1.util.pluralize('item', driversToSync.length, true)}`);
if (support_1.util.isEmpty(driversToSync)) {
return [];
}
}
const minBrowserVersionInt = (0, utils_1.convertToInt)(minBrowserVersion);
if (minBrowserVersionInt !== null) {
// Only select drivers that support the current browser whose major version number equals to `minBrowserVersion`
log.debug(`Selecting chromedrivers whose minimum supported browser version matches to ${minBrowserVersionInt}`);
let closestMatchedVersionNumber = 0;
// Select the newest available and compatible chromedriver
for (const cdName of driversToSync) {
const currentMinBrowserVersion = parseInt(String(this.mapping[cdName].minBrowserVersion), 10);
if (!Number.isNaN(currentMinBrowserVersion) &&
currentMinBrowserVersion <= minBrowserVersionInt &&
closestMatchedVersionNumber < currentMinBrowserVersion) {
closestMatchedVersionNumber = currentMinBrowserVersion;
}
}
driversToSync = driversToSync.filter((cdName) => `${this.mapping[cdName].minBrowserVersion}` ===
`${closestMatchedVersionNumber > 0 ? closestMatchedVersionNumber : minBrowserVersionInt}`);
log.debug(`Got ${support_1.util.pluralize('item', driversToSync.length, true)}`);
if (support_1.util.isEmpty(driversToSync)) {
return [];
}
log.debug(`Will select candidate ${support_1.util.pluralize('driver', driversToSync.length)} ` +
`versioned as '${support_1.util.uniq(driversToSync.map((cdName) => this.mapping[cdName].version))}'`);
}
if (!support_1.util.isEmpty(osInfo)) {
// Filter out drivers for unsupported system architectures
const { name, arch, cpu = (0, utils_1.getCpuType)() } = osInfo;
log.debug(`Selecting chromedrivers whose platform matches to ${name}:${cpu}${arch}`);
let result = driversToSync.filter((cdName) => this.doesMatchForOsInfo(cdName, osInfo));
if (support_1.util.isEmpty(result) && arch === constants_1.ARCH.X64 && cpu === constants_1.CPU.INTEL) {
// Fallback to X86 if X64 architecture is not available for this driver
result = driversToSync.filter((cdName) => this.doesMatchForOsInfo(cdName, {
name,
arch: constants_1.ARCH.X86,
cpu,
}));
}
if (support_1.util.isEmpty(result) && (name === constants_1.OS.MAC || name === constants_1.OS.WINDOWS) && cpu === constants_1.CPU.ARM) {
// Fallback to Intel (Rosetta on macOS, x64 emulation on Windows ARM):
// https://github.com/appium/appium-chromedriver/issues/562
result = driversToSync.filter((cdName) => this.doesMatchForOsInfo(cdName, {
name,
arch,
cpu: constants_1.CPU.INTEL,
}));
}
driversToSync = result;
log.debug(`Got ${support_1.util.pluralize('item', driversToSync.length, true)}`);
}
if (!support_1.util.isEmpty(driversToSync)) {
log.debug('Excluding older patches if present');
const patchesMap = {};
// Older chromedrivers must not be excluded as they follow a different
// versioning pattern
const versionWithPatchPattern = /\d+\.\d+\.\d+\.\d+/;
const selectedVersions = new Set();
for (const cdName of driversToSync) {
const cdVersion = this.mapping[cdName].version;
if (!versionWithPatchPattern.test(cdVersion)) {
selectedVersions.add(cdVersion);
continue;
}
const verObj = semver.parse(cdVersion, { loose: true });
if (!verObj) {
continue;
}
if (!Array.isArray(patchesMap[verObj.major])) {
patchesMap[verObj.major] = [];
}
patchesMap[verObj.major].push(cdVersion);
}
for (const majorVersion of Object.keys(patchesMap)) {
if (patchesMap[majorVersion].length <= 1) {
continue;
}
patchesMap[majorVersion].sort((a, b) => (0, compare_versions_1.compareVersions)(b, a));
}
if (!support_1.util.isEmpty(patchesMap)) {
log.debug('Versions mapping: ' + JSON.stringify(patchesMap, null, 2));
for (const sortedVersions of Object.values(patchesMap)) {
selectedVersions.add(sortedVersions[0]);
}
driversToSync = driversToSync.filter((cdName) => selectedVersions.has(this.mapping[cdName].version));
}
}
return driversToSync;
}
/**
* Checks whether the given chromedriver matches the operating system to run on
*
* @param cdName - The chromedriver entry key in the mapping
* @param osInfo - Operating system information to match against
* @returns True if the chromedriver matches the OS info
*/
doesMatchForOsInfo(cdName, { name, arch, cpu }) {
const cdInfo = this.mapping[cdName];
if (!cdInfo) {
return false;
}
if (cdInfo.os.name !== name || cdInfo.os.arch !== arch) {
return false;
}
if (cpu && cdInfo.os.cpu && this.mapping[cdName].os.cpu !== cpu) {
return false;
}
return true;
}
/**
* Retrieves the given chromedriver from the storage
* and unpacks it into `this.chromedriverDir` folder
*
* @param index - The unique driver index
* @param driverKey - The driver key in `this.mapping`
* @param archivesRoot - The temporary folder path to extract
* downloaded archives to
* @param isStrict [true] - Whether to throw an error (`true`)
* or return a boolean result if the driver retrieval process fails
* @throws {Error} if there was a failure while retrieving the driver
* and `isStrict` is set to `true`
* @returns if `true` then the chromedriver is successfully
* downloaded and extracted.
*/
async retrieveDriver(index, driverKey, archivesRoot, isStrict = false) {
const { url, etag, version } = this.mapping[driverKey];
const archivePath = node_path_1.default.resolve(archivesRoot, `${index}.zip`);
log.debug(`Retrieving '${url}' to '${archivePath}'`);
try {
await support_1.net.downloadFile(url, archivePath, {
isMetered: false,
timeout: constants_1.STORAGE_REQ_TIMEOUT_MS,
});
}
catch (e) {
const err = e;
const msg = `Cannot download chromedriver archive. Original error: ${err.message}`;
if (isStrict) {
throw new Error(msg, { cause: e });
}
log.error(msg);
return false;
}
if (etag && !(await isCrcOk(archivePath, etag))) {
const msg = `The checksum for the downloaded chromedriver '${driverKey}' did not match`;
if (isStrict) {
throw new Error(msg);
}
log.error(msg);
return false;
}
const fileName = `${node_path_1.default.parse(url).name}_v${version}` + (support_1.system.isWindows() ? '.exe' : '');
const targetPath = node_path_1.default.resolve(this.chromedriverDir, fileName);
try {
await this.unzipDriver(archivePath, targetPath);
await support_1.fs.chmod(targetPath, 0o755);
log.debug(`Permissions of the file '${targetPath}' have been changed to 755`);
}
catch (e) {
const err = e;
if (isStrict) {
throw err;
}
log.error(err.message);
return false;
}
return true;
}
/**
* Extracts downloaded chromedriver archive
* into the given destination
*
* @param src - The source archive path
* @param dst - The destination chromedriver path
*/
async unzipDriver(src, dst) {
const tmpRoot = await support_1.tempDir.openDir();
try {
await support_1.zip.extractAllTo(src, tmpRoot);
const chromedriverPath = await support_1.fs.walkDir(tmpRoot, true, (itemPath, isDirectory) => !isDirectory && node_path_1.default.parse(itemPath).name.toLowerCase() === 'chromedriver');
if (!chromedriverPath) {
throw new Error('The archive was unzipped properly, but we could not find any chromedriver executable');
}
log.debug(`Moving the extracted '${node_path_1.default.basename(chromedriverPath)}' to '${dst}'`);
await support_1.fs.mv(chromedriverPath, dst, {
mkdirp: true,
});
}
finally {
await support_1.fs.rimraf(tmpRoot);
}
}
}
exports.ChromedriverStorageClient = ChromedriverStorageClient;
async function isCrcOk(src, checksum) {
const md5 = await support_1.fs.hash(src, 'md5');
return md5.toLowerCase() === checksum.toLowerCase();
}
//# sourceMappingURL=storage-client.js.map