UNPKG

appium-chromedriver

Version:
420 lines 19.4 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 lodash_1 = __importDefault(require("lodash")); const bluebird_1 = __importDefault(require("bluebird")); const path_1 = __importDefault(require("path")); 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 = /** @type {readonly StorageInfo[]} */ ([{ 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'); /** * * @param {string} src * @param {string} checksum * @returns {Promise<boolean>} */ async function isCrcOk(src, checksum) { const md5 = await support_1.fs.hash(src, 'md5'); return lodash_1.default.toLower(md5) === lodash_1.default.toLower(checksum); } class ChromedriverStorageClient { /** * * @param {import('../types').ChromedriverStorageClientOpts} args */ constructor(args = {}) { const { chromedriverDir = (0, utils_1.getChromedriverDir)(), timeout = constants_1.STORAGE_REQ_TIMEOUT_MS } = args; this.chromedriverDir = chromedriverDir; this.timeout = timeout; /** @type {ChromedriverDetailsMapping} */ this.mapping = {}; } /** * Retrieves chromedriver mapping from the storage * * @param {boolean} 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) { /** @type {(si: StorageInfo) => Promise<string|undefined>} */ const retrieveResponseSafely = async (/** @type {StorageInfo} */ { url, accept }) => { try { return await (0, utils_1.retrieveData)(url, { 'user-agent': constants_1.USER_AGENT, accept: `${accept}, */*`, }, { timeout: this.timeout }); } catch (e) { log.debug(/** @type {Error} */ (e).stack); log.warn(`Cannot retrieve Chromedrivers info from ${url}. ` + `Make sure this URL is accessible from your network. ` + `Original error: ${ /** @type {Error} */(e).message}`); } }; const [xmlStr, jsonStr] = await bluebird_1.default.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 avilable ` + `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; } /** * Extracts downloaded chromedriver archive * into the given destination * * @param {string} src - The source archive path * @param {string} 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 && lodash_1.default.toLower(path_1.default.parse(itemPath).name) === 'chromedriver'); if (!chromedriverPath) { throw new Error('The archive was unzipped properly, but we could not find any chromedriver executable'); } log.debug(`Moving the extracted '${path_1.default.basename(chromedriverPath)}' to '${dst}'`); await support_1.fs.mv(chromedriverPath, dst, { mkdirp: true, }); } finally { await support_1.fs.rimraf(tmpRoot); } } /** * Filters `this.mapping` to only select matching * chromedriver entries by operating system information * and/or additional synchronization options (if provided) * * @param {OSInfo} osInfo * @param {SyncOptions} opts * @returns {Array<String>} The list of filtered chromedriver * entry names (version/archive name) */ selectMatchingDrivers(osInfo, opts = {}) { const { minBrowserVersion, versions = [] } = opts; let driversToSync = lodash_1.default.keys(this.mapping); if (!lodash_1.default.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 (lodash_1.default.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 (lodash_1.default.isEmpty(driversToSync)) { return []; } log.debug(`Will select candidate ${support_1.util.pluralize('driver', driversToSync.length)} ` + `versioned as '${lodash_1.default.uniq(driversToSync.map((cdName) => this.mapping[cdName].version))}'`); } if (!lodash_1.default.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 (lodash_1.default.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 (lodash_1.default.isEmpty(result) && name === constants_1.OS.MAC && cpu === constants_1.CPU.ARM) { // Fallback to Intel/Rosetta if ARM architecture is not available for this driver 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 (!lodash_1.default.isEmpty(driversToSync)) { log.debug('Excluding older patches if present'); /** @type {{[key: string]: string[]}} */ 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 (!lodash_1.default.isArray(patchesMap[verObj.major])) { patchesMap[verObj.major] = []; } patchesMap[verObj.major].push(cdVersion); } for (const majorVersion of lodash_1.default.keys(patchesMap)) { if (patchesMap[majorVersion].length <= 1) { continue; } patchesMap[majorVersion].sort((/** @type {string} */ a, /** @type {string}} */ b) => (0, compare_versions_1.compareVersions)(b, a)); } if (!lodash_1.default.isEmpty(patchesMap)) { log.debug('Versions mapping: ' + JSON.stringify(patchesMap, null, 2)); for (const sortedVersions of lodash_1.default.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 {string} cdName * @param {OSInfo} osInfo * @returns {boolean} */ 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 {number} index - The unique driver index * @param {string} driverKey - The driver key in `this.mapping` * @param {string} archivesRoot - The temporary folder path to extract * downloaded archives to * @param {boolean} 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 {Promise<boolean>} 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 = 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 = /** @type {Error} */ (e); const msg = `Cannot download chromedriver archive. Original error: ${err.message}`; if (isStrict) { throw new Error(msg); } 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 = `${path_1.default.parse(url).name}_v${version}` + (support_1.system.isWindows() ? '.exe' : ''); const targetPath = 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 = /** @type {Error} */ (e); if (isStrict) { throw err; } log.error(err.message); return false; } return true; } /** * Retrieves chromedrivers from the remote storage * to the local file system * * @param {SyncOptions} opts * @throws {Error} if there was a problem while retrieving * the drivers * @returns {Promise<string[]>} The list of successfully synchronized driver keys */ async syncDrivers(opts = {}) { if (lodash_1.default.isEmpty(this.mapping)) { await this.retrieveMapping(!!opts.minBrowserVersion); } if (lodash_1.default.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 (lodash_1.default.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)); /** * @type {string[]} */ const synchronizedDrivers = []; const promises = []; const chunk = []; const archivesRoot = await support_1.tempDir.openDir(); try { for (const [idx, driverKey] of driversToSync.entries()) { const promise = bluebird_1.default.resolve((async () => { if (await this.retrieveDriver(idx, driverKey, archivesRoot, !lodash_1.default.isEmpty(opts))) { synchronizedDrivers.push(driverKey); } })()); promises.push(promise); chunk.push(promise); if (chunk.length >= MAX_PARALLEL_DOWNLOADS) { await bluebird_1.default.any(chunk); } lodash_1.default.remove(chunk, (p) => p.isFulfilled()); } await bluebird_1.default.all(promises); } finally { await support_1.fs.rimraf(archivesRoot); } if (!lodash_1.default.isEmpty(synchronizedDrivers)) { log.info(`Successfully synchronized ` + `${support_1.util.pluralize('chromedriver', synchronizedDrivers.length, true)}`); } else { log.info(`No chromedrivers were synchronized`); } return synchronizedDrivers; } /** * Return latest chromedriver version for Chrome for Testing. * @returns {Promise<string>} */ 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 err = /** @type {Error} */ (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 enironment variable. Original error: ${err.message}`); } return (0, chromelabs_1.parseLatestKnownGoodVersionsJson)(jsonStr); } } exports.ChromedriverStorageClient = ChromedriverStorageClient; exports.default = ChromedriverStorageClient; /** * @typedef {import('../types').SyncOptions} SyncOptions * @typedef {import('../types').OSInfo} OSInfo * @typedef {import('../types').ChromedriverDetails} ChromedriverDetails * @typedef {import('../types').ChromedriverDetailsMapping} ChromedriverDetailsMapping */ /** * @typedef {Object} StorageInfo * @property {string} url * @property {string} accept */ //# sourceMappingURL=storage-client.js.map