UNPKG

appium-uiautomator2-driver

Version:
303 lines 14.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.allocateSystemPort = allocateSystemPort; exports.releaseSystemPort = releaseSystemPort; exports.allocateMjpegServerPort = allocateMjpegServerPort; exports.releaseMjpegServerPort = releaseMjpegServerPort; exports.performPreExecSetup = performPreExecSetup; exports.performExecution = performExecution; exports.performPostExecSetup = performPostExecSetup; exports.startSession = startSession; exports.initServer = initServer; exports.requireServer = requireServer; const io_appium_settings_1 = require("io.appium.settings"); const support_1 = require("appium/support"); const asyncbox_1 = require("asyncbox"); const node_os_1 = __importDefault(require("node:os")); const node_path_1 = __importDefault(require("node:path")); const portscanner_1 = require("portscanner"); const core_1 = require("./core"); const packages_1 = require("./packages"); // The range of ports we can use on the system for communicating to the // UiAutomator2 HTTP server on the device const DEVICE_PORT_RANGE = [8200, 8299]; // The guard is needed to avoid dynamic system port allocation conflicts for // parallel driver sessions const DEVICE_PORT_ALLOCATION_GUARD = support_1.util.getLockFileGuard(node_path_1.default.resolve(node_os_1.default.tmpdir(), 'uia2_device_port_guard'), { timeout: 25, tryRecovery: true }); // This is the port that UiAutomator2 listens to on the device. We will forward // one of the ports above on the system to this port on the device. const DEVICE_PORT = 6790; // This is the port that the UiAutomator2 MJPEG server listens to on the device. // We will forward one of the ports above on the system to this port on the // device. const MJPEG_SERVER_DEVICE_PORT = 7810; const MIN_SUPPORTED_API_LEVEL = 26; const LOCALHOST_IP4 = '127.0.0.1'; /** Forwards a local port to the UiAutomator2 server port on the device. */ async function allocateSystemPort() { const adb = this.requireAdb(); const forwardPort = async (localPort) => { this.log.debug(`Forwarding UiAutomator2 Server port ${DEVICE_PORT} to local port ${localPort}`); if ((await (0, portscanner_1.checkPortStatus)(localPort, LOCALHOST_IP4)) === 'open') { throw this.log.errorWithException(`UiAutomator2 Server cannot start because the local port #${localPort} is busy. ` + `Make sure the port you provide via 'systemPort' capability is not occupied. ` + `This situation might often be a result of an inaccurate sessions management, e.g. ` + `old automation sessions on the same device must always be closed before starting new ones.`); } await adb.forwardPort(localPort, DEVICE_PORT); }; if (this.opts.systemPort) { this.systemPort = this.opts.systemPort; return await forwardPort(this.systemPort); } await DEVICE_PORT_ALLOCATION_GUARD(async () => { const [startPort, endPort] = DEVICE_PORT_RANGE; try { this.systemPort = await (0, portscanner_1.findAPortNotInUse)(startPort, endPort); } catch { throw this.log.errorWithException(`Cannot find any free port in range ${startPort}..${endPort}}. ` + `Please set the available port number by providing the systemPort capability or ` + `double check the processes that are locking ports within this range and terminate ` + `these which are not needed anymore`); } await forwardPort(this.systemPort); }); } /** Removes the UiAutomator2 server port forward. */ async function releaseSystemPort() { const adb = this.adb; const systemPort = this.systemPort; if (!systemPort || !adb) { return; } if (this.opts.systemPort) { // We assume if the systemPort is provided manually then it must be unique, // so there is no need for the explicit synchronization await adb.removePortForward(systemPort); } else { await DEVICE_PORT_ALLOCATION_GUARD(async () => await adb.removePortForward(systemPort)); } } /** Forwards a local port to the UiAutomator2 MJPEG server port on the device. */ async function allocateMjpegServerPort() { if (this.opts.mjpegServerPort) { const adb = this.requireAdb(); this.log.debug(`MJPEG broadcasting requested, forwarding MJPEG server port ${MJPEG_SERVER_DEVICE_PORT} ` + `to local port ${this.opts.mjpegServerPort}`); await adb.forwardPort(this.opts.mjpegServerPort, MJPEG_SERVER_DEVICE_PORT); } } /** Removes the UiAutomator2 MJPEG server port forward. */ async function releaseMjpegServerPort() { if (this.opts.mjpegServerPort && this.adb) { await this.adb.removePortForward(this.opts.mjpegServerPort); } } /** Runs device preparation steps before the UiAutomator2 server session starts. */ async function performPreExecSetup() { const apiLevel = await this.adb.getApiLevel(); if (apiLevel < MIN_SUPPORTED_API_LEVEL) { throw this.log.errorWithException('UIAutomator2 only supports Android 8.0 (Oreo) and above'); } const preflightPromises = []; if (apiLevel >= 28) { // Android P preflightPromises.push((async () => { this.log.info('Relaxing hidden api policy'); try { await this.adb.setHiddenApiPolicy('1', !!this.opts.ignoreHiddenApiPolicyError); } catch (err) { throw this.log.errorWithException('Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. ' + 'This might be happening because the device under test is not configured properly. ' + 'Please check https://github.com/appium/appium/issues/13802 for more details. ' + 'You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ' + 'ignore this error, which might later lead to unexpected crashes or behavior of ' + `the automation server. Original error: ${err.message}`); } })()); } if (support_1.util.hasValue(this.opts.gpsEnabled)) { preflightPromises.push((async () => { this.log.info(`Trying to ${this.opts.gpsEnabled ? 'enable' : 'disable'} gps location provider`); await this.adb.toggleGPSLocationProvider(Boolean(this.opts.gpsEnabled)); })()); } if (this.opts.hideKeyboard) { preflightPromises.push((async () => { this._originalIme = await this.adb.defaultIME(); })()); } let appInfo; preflightPromises.push((async () => { // get appPackage et al from manifest if necessary appInfo = await this.getLaunchInfo(); })()); // start settings app, set the language/locale, start logcat etc... preflightPromises.push(this.initDevice()); await Promise.all(preflightPromises); this.opts = { ...this.opts, ...(appInfo ?? {}) }; return appInfo; } /** Installs and starts the UiAutomator2 server for the current session. */ async function performExecution(capsWithSessionInfo) { await Promise.all([ // Prepare the device by forwarding the UiAutomator2 port // This call mutates this.systemPort if it is not set explicitly this.allocateSystemPort(), // Prepare the device by forwarding the UiAutomator2 MJPEG server port (if // applicable) this.allocateMjpegServerPort(), ]); const [uiautomator2] = await Promise.all([ // set up the modified UiAutomator2 server etc this.initUiAutomator2Server(), (async () => { // Should be after installing io.appium.settings if (this.opts.disableWindowAnimation && (await this.adb.getApiLevel()) < 26) { // API level 26 is Android 8.0. // Granting android.permission.SET_ANIMATION_SCALE is necessary to handle animations under API level 26 // Read https://github.com/appium/appium/pull/11640#issuecomment-438260477 // `--no-window-animation` works over Android 8 to disable all of animations if (await this.adb.isAnimationOn()) { this.log.info('Disabling animation via io.appium.settings'); await this.settingsApp.setAnimationState(false); this._wasWindowAnimationDisabled = true; } else { this.log.info('Window animation is already disabled'); } } })(), // set up app under test // prepare our actual AUT, get it on the device, etc... this.initAUT(), ]); // launch UiAutomator2 and wait till its online and we have a session await uiautomator2.startSession(capsWithSessionInfo); // now that everything has started successfully, turn on proxying so all // subsequent session requests go straight to/from uiautomator2 this.jwpProxyActive = true; } /** Runs post-start steps after the UiAutomator2 server session is online. */ async function performPostExecSetup() { // Unlock the device after the session is started. if (!this.opts.skipUnlock) { // unlock the device to prepare it for testing await this.unlock(); } else { this.log.debug(`'skipUnlock' capability set, so skipping device unlock`); } if (this.isChromeSession) { // start a chromedriver session await this.startChromeSession(); } else if (this.opts.autoLaunch && this.opts.appPackage) { await this.ensureAppStarts(); } // if the initial orientation is requested, set it if (support_1.util.hasValue(this.opts.orientation)) { this.log.debug(`Setting initial orientation to '${this.opts.orientation}'`); await this.setOrientation(this.opts.orientation); } // if we want to immediately get into a webview, set our context // appropriately if (this.opts.autoWebview) { const viewName = this.defaultWebviewName(); const timeout = this.opts.autoWebviewTimeout || 2000; this.log.info(`Setting auto webview to context '${viewName}' with timeout ${timeout}ms`); await (0, asyncbox_1.retryInterval)(timeout / 500, 500, this.setContext.bind(this), viewName); } // We would like to notify about the initial context setting if ((await this.getCurrentContext()) === this.defaultContextName()) { await this.notifyBiDiContextChange(); } } /** Orchestrates UiAutomator2 server session startup and returns session capabilities. */ async function startSession(caps) { const appInfo = await this.performSessionPreExecSetup(); // set actual device name, udid, platform version, screen size, screen density, model and manufacturer details const deviceName = this.adb?.curDeviceId; const deviceUDID = this.opts.udid; if (!deviceName) { throw this.log.errorWithException('Could not determine device name (ADB curDeviceId is empty)'); } if (!deviceUDID) { throw this.log.errorWithException('Device UDID is not set in session options'); } const sessionInfo = { deviceName, deviceUDID, }; const capsWithSessionInfo = { ...caps, ...sessionInfo, }; // Adding AUT info in the capabilities if it does not exist in caps if (appInfo) { for (const capName of ['appPackage', 'appActivity']) { if (!capsWithSessionInfo[capName] && appInfo[capName]) { capsWithSessionInfo[capName] = appInfo[capName]; } } } await this.performSessionExecution(capsWithSessionInfo); const deviceInfoPromise = (async () => { try { return await this.getDeviceDetails(); } catch (e) { this.log.warn(`Cannot fetch device details. Original error: ${e.message}`); return {}; } })(); await this.performSessionPostExecSetup(); return { ...capsWithSessionInfo, ...(await deviceInfoPromise) }; } /** Creates the UiAutomator2 server client and installs server APKs when needed. */ async function initServer() { const uiautomator2Opts = { host: this.opts.remoteAdbHost || LOCALHOST_IP4, systemPort: this.systemPort, adb: this.adb, disableWindowAnimation: !!this.opts.disableWindowAnimation, disableSuppressAccessibilityService: this.opts.disableSuppressAccessibilityService, readTimeout: this.opts.uiautomator2ServerReadTimeout, basePath: this.basePath, }; // now that we have package and activity, we can create an instance of // uiautomator2 with the appropriate options this.uiautomator2 = new core_1.UiAutomator2Server(this.log, uiautomator2Opts); this.proxyReqRes = this.uiautomator2.proxyReqRes.bind(this.uiautomator2); this.proxyCommand = this.uiautomator2.proxyCommand.bind(this.uiautomator2); if (this.opts.skipServerInstallation) { this.log.info(`'skipServerInstallation' is set. Skipping UIAutomator2 server installation.`); } else { await this.uiautomator2.installServerApk(this.opts.uiautomator2ServerInstallTimeout); try { await this.requireAdb().addToDeviceIdleWhitelist(io_appium_settings_1.SETTINGS_HELPER_ID, packages_1.SERVER_PACKAGE_ID, packages_1.SERVER_TEST_PACKAGE_ID); } catch (e) { const err = e; this.log.warn(`Cannot add server packages to the Doze whitelist. Original error: ` + (err.stderr || err.message)); } } return this.uiautomator2; } /** Returns the initialized UiAutomator2 server client or throws if it is missing. */ function requireServer() { const server = this.uiautomator2; if (!server) { throw this.log.errorWithException('UiAutomator2 server is not initialized'); } return server; } //# sourceMappingURL=session.js.map