UNPKG

appium-android-driver

Version:

Android UiAutomator and Chrome support for Appium

716 lines 31.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEVTOOLS_SOCKET_PATTERN = exports.WEBVIEW_BASE = exports.CHROMIUM_WIN = exports.WEBVIEW_WIN = exports.NATIVE_WIN = exports.KNOWN_CHROME_PACKAGE_NAMES = exports.CHROME_PACKAGE_NAME = exports.CHROME_BROWSER_PACKAGE_ACTIVITY = void 0; exports.getChromePkg = getChromePkg; exports.parseWebviewNames = parseWebviewNames; exports.getWebViewsMapping = getWebViewsMapping; exports.setupNewChromedriver = setupNewChromedriver; exports.setupExistingChromedriver = setupExistingChromedriver; exports.shouldDismissChromeWelcome = shouldDismissChromeWelcome; exports.dismissChromeWelcome = dismissChromeWelcome; const support_1 = require("@appium/support"); const lodash_1 = __importDefault(require("lodash")); const axios_1 = __importDefault(require("axios")); const node_net_1 = __importDefault(require("node:net")); const portscanner_1 = require("portscanner"); const asyncbox_1 = require("asyncbox"); const bluebird_1 = __importDefault(require("bluebird")); const node_os_1 = __importDefault(require("node:os")); const node_path_1 = __importDefault(require("node:path")); const node_http_1 = __importDefault(require("node:http")); const appium_chromedriver_1 = require("appium-chromedriver"); const cache_1 = require("./cache"); const promises_1 = __importDefault(require("node:dns/promises")); // https://cs.chromium.org/chromium/src/chrome/browser/devtools/device/android_device_info_query.cc exports.CHROME_BROWSER_PACKAGE_ACTIVITY = { chrome: { pkg: 'com.android.chrome', activity: 'com.google.android.apps.chrome.Main', }, chromium: { pkg: 'org.chromium.chrome.shell', activity: '.ChromeShellActivity', }, chromebeta: { pkg: 'com.chrome.beta', activity: 'com.google.android.apps.chrome.Main', }, browser: { pkg: 'com.android.browser', activity: 'com.android.browser.BrowserActivity', }, 'chromium-browser': { pkg: 'org.chromium.chrome', activity: 'com.google.android.apps.chrome.Main', }, 'chromium-webview': { pkg: 'org.chromium.webview_shell', activity: 'org.chromium.webview_shell.WebViewBrowserActivity', }, default: { pkg: 'com.android.chrome', activity: 'com.google.android.apps.chrome.Main', }, }; exports.CHROME_PACKAGE_NAME = 'com.android.chrome'; exports.KNOWN_CHROME_PACKAGE_NAMES = [ exports.CHROME_PACKAGE_NAME, 'com.chrome.beta', 'com.chrome.dev', 'com.chrome.canary', ]; const CHROMEDRIVER_AUTODOWNLOAD_FEATURE = 'chromedriver_autodownload'; const CROSSWALK_SOCKET_PATTERN = /@([\w.]+)_devtools_remote\b/; const CHROMIUM_DEVTOOLS_SOCKET = 'chrome_devtools_remote'; exports.NATIVE_WIN = 'NATIVE_APP'; exports.WEBVIEW_WIN = 'WEBVIEW'; exports.CHROMIUM_WIN = 'CHROMIUM'; exports.WEBVIEW_BASE = `${exports.WEBVIEW_WIN}_`; exports.DEVTOOLS_SOCKET_PATTERN = /@[\w.]+_devtools_remote_?([\w.]+_)?(\d+)?\b/; const WEBVIEW_PID_PATTERN = new RegExp(`^${exports.WEBVIEW_BASE}(\\d+)`); const WEBVIEW_PKG_PATTERN = new RegExp(`^${exports.WEBVIEW_BASE}([^\\d\\s][\\w.]*)`); const WEBVIEW_WAIT_INTERVAL_MS = 200; const CDP_REQ_TIMEOUT = 2000; // ms const DEVTOOLS_PORTS_RANGE = [10900, 11000]; const DEVTOOLS_PORT_ALLOCATION_GUARD = support_1.util.getLockFileGuard(node_path_1.default.resolve(node_os_1.default.tmpdir(), 'android_devtools_port_guard'), { timeout: 7, tryRecovery: true }); // #region Exported Functions /** * Gets the Chrome browser package and activity information for the specified browser name. * * @param browser - The browser name (e.g., 'chrome', 'chromium', 'chromebeta') * @returns The package and activity information for the browser, or the default Chrome configuration */ function getChromePkg(browser) { return (exports.CHROME_BROWSER_PACKAGE_ACTIVITY[browser.toLowerCase()] || exports.CHROME_BROWSER_PACKAGE_ACTIVITY.default); } /** * Parses webview names from the webviews mapping for use in getContexts. * Filters out webviews that don't have pages if ensureWebviewsHavePages is enabled. * * @param webviewsMapping - Array of webview mapping objects * @param options - Options including ensureWebviewsHavePages and isChromeSession flags * @returns An array of webview context names */ function parseWebviewNames(webviewsMapping, options = {}) { const { ensureWebviewsHavePages = true, isChromeSession = false } = options; if (isChromeSession) { return [exports.CHROMIUM_WIN]; } const result = []; for (const { webview, pages, proc, webviewName } of webviewsMapping) { if (ensureWebviewsHavePages && !pages?.length) { this.log.info(`Skipping the webview '${webview}' at '${proc}' ` + `since it has reported having zero pages`); continue; } if (webviewName) { result.push(webviewName); } } this.log.debug(`Found ${support_1.util.pluralize('webview', result.length, true)}: ${JSON.stringify(result)}`); return result; } /** * Get a list of available webviews mapping by introspecting processes with adb, * where webviews are listed. It's possible to pass in a 'deviceSocket' arg, which * limits the webview possibilities to the one running on the Chromium devtools * socket we're interested in (see note on webviewsFromProcs). We can also * direct this method to verify whether a particular webview process actually * has any pages (if a process exists but no pages are found, Chromedriver will * not actually be able to connect to it, so this serves as a guard for that * strange failure mode). The strategy for checking whether any pages are * active involves sending a request to the remote debug server on the device, * hence it is also possible to specify the port on the host machine which * should be used for this communication. * * @param opts - Options for webview discovery including device socket, port, and collection settings * @returns An array of webview mapping objects */ async function getWebViewsMapping(opts = {}) { const { androidDeviceSocket = null, ensureWebviewsHavePages = true, webviewDevtoolsPort = null, enableWebviewDetailsCollection = true, waitForWebviewMs = 0, } = opts; this.log.debug(`Getting a list of available webviews`); let waitMs = waitForWebviewMs; if (!lodash_1.default.isNumber(waitMs)) { waitMs = parseInt(`${waitMs}`, 10) || 0; } let webviewsMapping; const timer = new support_1.timing.Timer().start(); do { webviewsMapping = await webviewsFromProcs.bind(this)(androidDeviceSocket); if (webviewsMapping.length > 0) { break; } this.log.debug(`No webviews found in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); await (0, asyncbox_1.sleep)(WEBVIEW_WAIT_INTERVAL_MS); } while (timer.getDuration().asMilliSeconds < waitMs); await collectWebviewsDetails.bind(this)(webviewsMapping, { ensureWebviewsHavePages, enableWebviewDetailsCollection, webviewDevtoolsPort, }); for (const webviewMapping of webviewsMapping) { const { webview, info } = webviewMapping; webviewMapping.webviewName = null; let wvName = webview; let process; if (!androidDeviceSocket) { const pkgMatch = WEBVIEW_PKG_PATTERN.exec(webview); try { // web view name could either be suffixed with PID or the package name // package names could not start with a digit const pkg = pkgMatch ? pkgMatch[1] : await procFromWebview.bind(this)(webview); wvName = `${exports.WEBVIEW_BASE}${pkg}`; const pidMatch = WEBVIEW_PID_PATTERN.exec(webview); process = { name: pkg, id: pidMatch ? pidMatch[1] : null, }; } catch (e) { const err = e; this.log.debug(err.stack); this.log.warn(err.message); continue; } } webviewMapping.webviewName = wvName; const key = (0, cache_1.toDetailsCacheKey)(this.adb, wvName); if (info || process) { cache_1.WEBVIEWS_DETAILS_CACHE.set(key, { info, process }); } else if (cache_1.WEBVIEWS_DETAILS_CACHE.has(key)) { cache_1.WEBVIEWS_DETAILS_CACHE.delete(key); } } return webviewsMapping; } /** * Sets up a new Chromedriver instance for the specified context. * * @param opts - Driver options with Chrome-specific settings * @param curDeviceId - The current device ID * @param context - Optional context name for webview sessions * @returns A configured Chromedriver instance */ async function setupNewChromedriver(opts, curDeviceId, context) { // TODO: Remove the legacy if (opts.chromeDriverPort) { this.log.warn(`The 'chromeDriverPort' capability is deprecated. Please use 'chromedriverPort' instead`); opts.chromedriverPort = opts.chromeDriverPort; } if (opts.chromedriverPort) { this.log.debug(`Using user-specified port ${opts.chromedriverPort} for chromedriver`); } else { // if a single port wasn't given, we'll look for a free one opts.chromedriverPort = await getChromedriverPort.bind(this)(opts.chromedriverPorts); } const details = context ? (0, cache_1.getWebviewDetails)(this.adb, context) : undefined; if (!lodash_1.default.isEmpty(details)) { this.log.debug('Passing web view details to the Chromedriver constructor: ' + JSON.stringify(details, null, 2)); } const chromedriverOpts = { port: lodash_1.default.isNil(opts.chromedriverPort) ? undefined : String(opts.chromedriverPort), executable: opts.chromedriverExecutable, adb: this.adb, cmdArgs: opts.chromedriverArgs, verbose: !!opts.showChromedriverLog, executableDir: opts.chromedriverExecutableDir, mappingPath: opts.chromedriverChromeMappingFile, bundleId: opts.chromeBundleId, useSystemExecutable: opts.chromedriverUseSystemExecutable, disableBuildCheck: opts.chromedriverDisableBuildCheck, details: details, isAutodownloadEnabled: isChromedriverAutodownloadEnabled.bind(this)(), }; if (this.basePath) { chromedriverOpts.reqBasePath = this.basePath; } const chromedriver = new appium_chromedriver_1.Chromedriver(chromedriverOpts); // make sure there are chromeOptions opts.chromeOptions = opts.chromeOptions || {}; // try out any prefixed chromeOptions, // and strip the prefix for (const opt of lodash_1.default.keys(opts)) { if (opt.endsWith(':chromeOptions')) { this?.log?.warn(`Merging '${opt}' into 'chromeOptions'. This may cause unexpected behavior`); lodash_1.default.merge(opts.chromeOptions, opts[opt]); } } // Ensure there are logging preferences opts.chromeLoggingPrefs = opts.chromeLoggingPrefs ?? {}; // Strip the prefix and store it for (const opt of lodash_1.default.keys(opts)) { if (opt.endsWith(':loggingPrefs')) { this.log.warn(`Merging '${opt}' into 'chromeLoggingPrefs'. This may cause unexpected behavior`); lodash_1.default.merge(opts.chromeLoggingPrefs, opts[opt]); } } const caps = createChromedriverCaps.bind(this)(opts, curDeviceId, details); this.log.debug(`Before starting chromedriver, androidPackage is '${caps.chromeOptions.androidPackage}'`); const sessionCaps = await chromedriver.start(caps); cacheChromedriverCaps.bind(this)(sessionCaps, context); return chromedriver; } /** * Sets up an existing Chromedriver instance, checking if it's still working. * If not, restarts the session. * * @param chromedriver - The existing Chromedriver instance * @param context - The context name associated with this Chromedriver * @returns The Chromedriver instance (possibly restarted) */ async function setupExistingChromedriver(chromedriver, context) { // check the status by sending a simple window-based command to ChromeDriver // if there is an error, we want to recreate the ChromeDriver session if (await chromedriver.hasWorkingWebview()) { const cachedCaps = this._chromedriverCapsCache.get(context); if (cachedCaps) { cacheChromedriverCaps.bind(this)(cachedCaps, context); } } else { this.log.debug('ChromeDriver is not associated with a window. Re-initializing the session.'); const sessionCaps = await chromedriver.restart(); cacheChromedriverCaps.bind(this)(sessionCaps, context); } return chromedriver; } /** * Determines if the Chrome welcome dialog should be dismissed based on Chrome options. * * @returns True if the '--no-first-run' argument is present in chromeOptions */ function shouldDismissChromeWelcome() { return (!!this.opts.chromeOptions && lodash_1.default.isArray(this.opts.chromeOptions.args) && this.opts.chromeOptions.args.includes('--no-first-run')); } /** * Dismisses the Chrome welcome dialog if it appears. * Handles both the terms acceptance and sign-in dialog. */ async function dismissChromeWelcome() { this.log.info('Trying to dismiss Chrome welcome'); const activity = await this.getCurrentActivity(); if (activity !== 'org.chromium.chrome.browser.firstrun.FirstRunActivity') { this.log.info('Chrome welcome dialog never showed up! Continuing'); return; } const el = await this.findElOrEls('id', 'com.android.chrome:id/terms_accept', false); await this.click(el.ELEMENT); try { const el2 = await this.findElOrEls('id', 'com.android.chrome:id/negative_button', false); await this.click(el2.ELEMENT); } catch (e) { // DO NOTHING, THIS DEVICE DIDNT LAUNCH THE SIGNIN DIALOG // IT MUST BE A NON GMS DEVICE this.log.warn(`This device did not show Chrome SignIn dialog, ${e.message}`); } } // #endregion // #region Internal Helper Functions async function getFreePort() { return await new Promise((resolve, reject) => { const srv = node_net_1.default.createServer(); srv.listen(0, () => { const address = srv.address(); let port; if (address && typeof address === 'object' && 'port' in address) { port = address.port; } else { reject(new Error('Cannot determine any free port number')); return; } srv.close(() => resolve(port)); }); }); } /** * https://chromedevtools.github.io/devtools-protocol/ */ async function cdpGetRequest(host, port, endpoint) { // Workaround for https://github.com/puppeteer/puppeteer/issues/2242, https://github.com/appium/appium/issues/20782 const compatibleHost = isCompatibleCdpHost(host) ? host : (await promises_1.default.lookup(host)).address; return (await (0, axios_1.default)({ url: `http://${compatibleHost}:${port}${endpoint}`, timeout: CDP_REQ_TIMEOUT, // We need to set this from Node.js v19 onwards. // Otherwise, in situation with multiple webviews, // the preceding webview pages will be incorrectly retrieved as the current ones. // https://nodejs.org/en/blog/announcements/v19-release-announce#https11-keepalive-by-default httpAgent: new node_http_1.default.Agent({ keepAlive: false }), })).data; } /** */ async function cdpList(host, port) { return cdpGetRequest(host, port, '/json/list'); } /** */ async function cdpInfo(host, port) { return cdpGetRequest(host, port, '/json/version'); } /** * Creates Chromedriver capabilities based on the provided Appium capabilities. */ function createChromedriverCaps(opts, deviceId, webViewDetails) { const caps = { chromeOptions: {} }; const androidPackage = opts.chromeOptions?.androidPackage || opts.appPackage || webViewDetails?.info?.['Android-Package']; if (androidPackage) { // chromedriver raises an invalid argument error when androidPackage is 'null' caps.chromeOptions.androidPackage = androidPackage; } if (lodash_1.default.isBoolean(opts.chromeUseRunningApp)) { caps.chromeOptions.androidUseRunningApp = opts.chromeUseRunningApp; } if (opts.chromeAndroidPackage) { caps.chromeOptions.androidPackage = opts.chromeAndroidPackage; } if (opts.chromeAndroidActivity) { caps.chromeOptions.androidActivity = opts.chromeAndroidActivity; } if (opts.chromeAndroidProcess) { caps.chromeOptions.androidProcess = opts.chromeAndroidProcess; } else if (webViewDetails?.process?.name && webViewDetails?.process?.id) { caps.chromeOptions.androidProcess = webViewDetails.process.name; } if (lodash_1.default.toLower(opts.browserName) === 'chromium-webview') { caps.chromeOptions.androidActivity = opts.appActivity; } if (opts.pageLoadStrategy) { caps.pageLoadStrategy = opts.pageLoadStrategy; } const isChrome = lodash_1.default.toLower(caps.chromeOptions.androidPackage) === 'chrome'; if (lodash_1.default.includes(exports.KNOWN_CHROME_PACKAGE_NAMES, caps.chromeOptions.androidPackage) || isChrome) { // if we have extracted package from context name, it could come in as bare // "chrome", and so we should make sure the details are correct, including // not using an activity or process id if (isChrome) { caps.chromeOptions.androidPackage = exports.CHROME_PACKAGE_NAME; } delete caps.chromeOptions.androidActivity; delete caps.chromeOptions.androidProcess; } // add device id from adb caps.chromeOptions.androidDeviceSerial = deviceId; if (lodash_1.default.isPlainObject(opts.loggingPrefs) || lodash_1.default.isPlainObject(opts.chromeLoggingPrefs)) { if (opts.loggingPrefs) { this.log.warn(`The 'loggingPrefs' cap is deprecated; use the 'chromeLoggingPrefs' cap instead`); } caps.loggingPrefs = opts.chromeLoggingPrefs || opts.loggingPrefs; } if (opts.enablePerformanceLogging) { this.log.warn(`The 'enablePerformanceLogging' cap is deprecated; simply use ` + `the 'chromeLoggingPrefs' cap instead, with a 'performance' key set to 'ALL'`); const newPref = { performance: 'ALL' }; // don't overwrite other logging prefs that have been sent in if they exist caps.loggingPrefs = caps.loggingPrefs ? { ...caps.loggingPrefs, ...newPref } : newPref; } if (opts.chromeOptions?.Arguments) { // merge `Arguments` and `args` opts.chromeOptions.args = [...(opts.chromeOptions.args || []), ...opts.chromeOptions.Arguments]; delete opts.chromeOptions.Arguments; } if (opts.webSocketUrl && lodash_1.default.isNil(caps.webSocketUrl) && this.opts.chromedriverForwardBiDi) { caps.webSocketUrl = opts.webSocketUrl; } this.log.debug('Precalculated Chromedriver capabilities: ' + JSON.stringify(caps.chromeOptions, null, 2)); const protectedCapNames = []; for (const [opt, val] of lodash_1.default.toPairs(opts.chromeOptions)) { if (lodash_1.default.isUndefined(caps.chromeOptions[opt])) { caps.chromeOptions[opt] = val; } else { protectedCapNames.push(opt); } } if (!lodash_1.default.isEmpty(protectedCapNames)) { this.log.info('The following Chromedriver capabilities cannot be overridden ' + 'by the provided chromeOptions:'); for (const optName of protectedCapNames) { this.log.info(` ${optName} (${JSON.stringify(opts.chromeOptions[optName])})`); } } return caps; } /** * Allocates a local port for devtools communication. * * @param socketName - The remote Unix socket name * @param webviewDevtoolsPort - The local port number or null to apply autodetection * @returns The host name and the port number to connect to if the remote socket has been forwarded successfully * @throws {Error} If there was an error while allocating the local port */ async function allocateDevtoolsChannel(socketName, webviewDevtoolsPort = null) { // socket names come with '@', but this should not be a part of the abstract // remote port, so remove it const remotePort = socketName.replace(/^@/, ''); let startPort = DEVTOOLS_PORTS_RANGE[0]; let endPort = DEVTOOLS_PORTS_RANGE[1]; if (webviewDevtoolsPort) { endPort = webviewDevtoolsPort + (endPort - startPort); startPort = webviewDevtoolsPort; } this.log.debug(`Forwarding remote port ${remotePort} to a local ` + `port in range ${startPort}..${endPort}`); if (!webviewDevtoolsPort) { this.log.debug(`You could use the 'webviewDevtoolsPort' capability to customize ` + `the starting port number`); } const port = await DEVTOOLS_PORT_ALLOCATION_GUARD(async () => { let localPort; try { localPort = await (0, portscanner_1.findAPortNotInUse)(startPort, endPort); } catch { throw new Error(`Cannot find any free port to forward the Devtools socket ` + `in range ${startPort}..${endPort}. You could set the starting port number ` + `manually by providing the 'webviewDevtoolsPort' capability`); } await this.adb.adbExec(['forward', `tcp:${localPort}`, `localabstract:${remotePort}`]); return localPort; }); return [this.adb.adbHost ?? '127.0.0.1', port]; } /** * Wrapper for Chrome Debugger Protocol data collection. * No error is thrown if CDP request fails - in such case no data will be * recorded into the corresponding `webviewsMapping` item. * * @param webviewsMapping - The current webviews mapping. Each item of this array gets mutated * (`info`/`pages` properties get added based on the provided `opts`) * if the requested details have been successfully retrieved for it * @param opts - If both `ensureWebviewsHavePages` and `enableWebviewDetailsCollection` * properties are falsy then no details collection is performed */ async function collectWebviewsDetails(webviewsMapping, opts = {}) { if (lodash_1.default.isEmpty(webviewsMapping)) { return; } const { webviewDevtoolsPort = null, ensureWebviewsHavePages = null, enableWebviewDetailsCollection = null, } = opts; if (!ensureWebviewsHavePages) { this.log.info(`Not checking whether webviews have active pages; use the ` + `'ensureWebviewsHavePages' cap to turn this check on`); } if (!enableWebviewDetailsCollection) { this.log.info(`Not collecting web view details. Details collection might help ` + `to make Chromedriver initialization more precise. Use the 'enableWebviewDetailsCollection' ` + `cap to turn it on`); } if (!ensureWebviewsHavePages && !enableWebviewDetailsCollection) { return; } // Connect to each devtools socket and retrieve web view details this.log.debug(`Collecting CDP data of ${support_1.util.pluralize('webview', webviewsMapping.length, true)}`); const detailCollectors = []; for (const item of webviewsMapping) { detailCollectors.push((async () => { let port; let host; try { [host, port] = (await allocateDevtoolsChannel.bind(this)(item.proc, webviewDevtoolsPort)); if (enableWebviewDetailsCollection) { item.info = await cdpInfo(host, port); } if (ensureWebviewsHavePages) { item.pages = await cdpList(host, port); } } catch (e) { const err = e; this.log.info(`CDP data for '${item.webview}' cannot be collected. Original error: ${err.message}`); } finally { if (port) { try { await this.adb.removePortForward(port); } catch (e) { this.log.debug(e); } } } })()); } await bluebird_1.default.all(detailCollectors); this.log.debug(`CDP data collection completed`); } /** * Takes a webview name like WEBVIEW_4296 and uses 'adb shell ps' to figure out * which app package is associated with that webview. */ async function procFromWebview(webview) { const pidMatch = WEBVIEW_PID_PATTERN.exec(webview); if (!pidMatch) { throw new Error(`Could not find PID for webview '${webview}'`); } const pid = pidMatch[1]; this.log.debug(`${webview} mapped to pid ${pid}`); this.log.debug(`Getting process name for webview '${webview}'`); const pkg = await this.adb.getProcessNameById(pid); this.log.debug(`Got process name: '${pkg}'`); return pkg; } /** * Gets a list of Android system processes that look like webviews. * See https://cs.chromium.org/chromium/src/chrome/browser/devtools/device/android_device_info_query.cc * for more details. */ async function getPotentialWebviewProcs() { const out = await this.adb.shell(['cat', '/proc/net/unix']); const names = []; const allMatches = []; for (const line of out.split('\n')) { // Num RefCount Protocol Flags Type St Inode Path const [, , , flags, , st, , sockPath] = line.trim().split(/\s+/); if (!sockPath) { continue; } if (sockPath.startsWith('@')) { allMatches.push(line.trim()); } if (flags !== '00010000' || st !== '01') { continue; } if (!exports.DEVTOOLS_SOCKET_PATTERN.test(sockPath)) { continue; } names.push(sockPath); } if (lodash_1.default.isEmpty(names)) { this.log.debug('Found no active devtools sockets'); if (!lodash_1.default.isEmpty(allMatches)) { this.log.debug(`Other sockets are: ${JSON.stringify(allMatches, null, 2)}`); } } else { this.log.debug(`Parsed ${names.length} active devtools ${support_1.util.pluralize('socket', names.length, false)}: ` + JSON.stringify(names)); } // sometimes the webview process shows up multiple times per app return lodash_1.default.uniq(names); } /** * Retrieves a list of system processes that look like webviews, * and returns them along with the webview context name appropriate for it. * If a deviceSocket is provided, only attempts to find webviews which match * that socket name (for apps which embed Chromium, which isn't the * same as chrome-backed webviews). * * @param deviceSocket - The explicitly-named device socket to use, or null to find all webviews * @returns An array of webview process objects with proc and webview properties */ async function webviewsFromProcs(deviceSocket = null) { const socketNames = await getPotentialWebviewProcs.bind(this)(); const webviews = []; for (const socketName of socketNames) { if (deviceSocket === CHROMIUM_DEVTOOLS_SOCKET && socketName === `@${deviceSocket}`) { webviews.push({ proc: socketName, webview: exports.CHROMIUM_WIN, }); continue; } const socketNameMatch = exports.DEVTOOLS_SOCKET_PATTERN.exec(socketName); if (!socketNameMatch) { continue; } const matchedSocketName = socketNameMatch[2]; const crosswalkMatch = CROSSWALK_SOCKET_PATTERN.exec(socketName); if (!matchedSocketName && !crosswalkMatch) { continue; } if ((deviceSocket && socketName === `@${deviceSocket}`) || !deviceSocket) { webviews.push({ proc: socketName, webview: matchedSocketName ? `${exports.WEBVIEW_BASE}${matchedSocketName}` : `${exports.WEBVIEW_BASE}${crosswalkMatch?.[1] ?? ''}`, }); } } return webviews; } /** * Finds a free port for Chromedriver based on the provided port specification. * If no specification is provided, finds any available free port. */ async function getChromedriverPort(portSpec) { // if the user didn't give us any specific information about chromedriver // port ranges, just find any free port if (!portSpec) { const port = await getFreePort(); this.log.debug(`A port was not given, using random free port: ${port}`); return port; } // otherwise find the free port based on a list or range provided by the user this.log.debug(`Finding a free port for chromedriver using spec ${JSON.stringify(portSpec)}`); let foundPort = null; for (const potentialPort of portSpec) { let port; let stopPort; if (Array.isArray(potentialPort)) { [port, stopPort] = potentialPort.map((p) => parseInt(String(p), 10)); } else { port = parseInt(String(potentialPort), 10); // ensure we have a number and not a string stopPort = port; } this.log.debug(`Checking port range ${port}:${stopPort}`); try { foundPort = await (0, portscanner_1.findAPortNotInUse)(port, stopPort); break; } catch { this.log.debug(`Nothing in port range ${port}:${stopPort} was available`); } } if (foundPort === null) { throw new Error(`Could not find a free port for chromedriver using ` + `chromedriverPorts spec ${JSON.stringify(portSpec)}`); } this.log.debug(`Using free port ${foundPort} for chromedriver`); return foundPort; } function isChromedriverAutodownloadEnabled() { if (this.isFeatureEnabled(CHROMEDRIVER_AUTODOWNLOAD_FEATURE)) { return true; } this.log.debug(`Automated Chromedriver download is disabled. ` + `Use '${CHROMEDRIVER_AUTODOWNLOAD_FEATURE}' server feature to enable it`); return false; } function cacheChromedriverCaps(sessionCaps, context) { if (!context) { return; } // Store full session capabilities in cache this._chromedriverCapsCache.set(context, sessionCaps); if (this.opts?.chromedriverForwardBiDi && sessionCaps?.webSocketUrl && this._bidiProxyUrl !== sessionCaps.webSocketUrl) { this._bidiProxyUrl = sessionCaps.webSocketUrl; this.log.debug(`Updated Bidi Proxy URL to ${this._bidiProxyUrl}`); } } /** * https://github.com/puppeteer/puppeteer/issues/2242#issuecomment-544219536 */ function isCompatibleCdpHost(host) { return (['localhost', 'localhost.localdomain'].includes(host) || host.endsWith('.localhost') || Boolean(node_net_1.default.isIP(host))); } // #endregion //# sourceMappingURL=helpers.js.map