appium-android-driver
Version:
Android UiAutomator and Chrome support for Appium
716 lines • 31.3 kB
JavaScript
"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