UNPKG

appium-chromedriver

Version:
390 lines 18.3 kB
"use strict"; 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