UNPKG

appium-uiautomator2-driver

Version:
414 lines 18.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UiAutomator2Server = exports.INSTRUMENTATION_TARGET = void 0; const driver_1 = require("appium/driver"); const asyncbox_1 = require("asyncbox"); const appium_uiautomator2_server_1 = require("appium-uiautomator2-server"); const support_1 = require("appium/support"); const axios_1 = __importDefault(require("axios")); const packages_1 = require("./packages"); const SERVER_LAUNCH_TIMEOUT_MS = 30000; const SERVER_INSTALL_RETRIES = 20; const SERVICES_LAUNCH_TIMEOUT_MS = 30000; const SERVER_SHUTDOWN_TIMEOUT_MS = 5000; const SERVER_REQUEST_TIMEOUT_MS = 500; exports.INSTRUMENTATION_TARGET = `${packages_1.SERVER_TEST_PACKAGE_ID}/androidx.test.runner.AndroidJUnitRunner`; const REQUIRED_OPTIONS = [ 'adb', 'host', 'systemPort', 'disableWindowAnimation', ]; class UIA2Proxy extends driver_1.JWProxy { didInstrumentationExit = false; async proxyCommand(url, method, body = null) { if (this.didInstrumentationExit) { throw new driver_1.errors.InvalidContextError(`'${method} ${url}' cannot be proxied to UiAutomator2 server because ` + 'the instrumentation process is not running (probably crashed). ' + 'Check the server log and/or the logcat output for more details'); } return await super.proxyCommand(url, method, body); } } class UiAutomator2Server { jwproxy; proxyReqRes; proxyCommand; host; systemPort; adb; disableWindowAnimation; disableSuppressAccessibilityService; log; instrumentationProcess = null; constructor(log, opts) { // Validate and assign required properties from UiAutomator2ServerOptions // The keys are typed to match only the required (non-optional) properties of the interface for (const key of REQUIRED_OPTIONS) { if (!opts || !support_1.util.hasValue(opts[key])) { throw new Error(`Option '${key}' is required!`); } this[key] = opts[key]; } this.log = log; this.disableSuppressAccessibilityService = opts.disableSuppressAccessibilityService; const proxyOpts = { log, server: this.host, port: this.systemPort, keepAlive: true, }; if (opts.basePath) { proxyOpts.reqBasePath = opts.basePath; } if (opts.readTimeout && opts.readTimeout > 0) { proxyOpts.timeout = opts.readTimeout; } this.jwproxy = new UIA2Proxy(proxyOpts); this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy); this.proxyCommand = this.jwproxy.command.bind(this.jwproxy); this.jwproxy.didInstrumentationExit = false; this.instrumentationProcess = null; } /** * Installs the apks on to the device or emulator. * * @param installTimeout - Installation timeout */ async installServerApk(installTimeout = SERVER_INSTALL_RETRIES * 1000) { const packagesInfo = await Promise.all([ { appPath: appium_uiautomator2_server_1.SERVER_APK_PATH, appId: packages_1.SERVER_PACKAGE_ID, }, { appPath: appium_uiautomator2_server_1.TEST_APK_PATH, appId: packages_1.SERVER_TEST_PACKAGE_ID, }, ].map(({ appPath, appId }) => this.prepareServerPackage(appPath, appId))); this.log.debug(`Server packages status: ${JSON.stringify(packagesInfo)}`); const shouldUninstallServerPackages = this.shouldUninstallServerPackages(packagesInfo); // Install must always follow uninstall. Also, perform the install if // any of server packages is not installed or is outdated const shouldInstallServerPackages = shouldUninstallServerPackages || this.shouldInstallServerPackages(packagesInfo); this.log.info(`Server packages are ${shouldInstallServerPackages ? '' : 'not '}going to be (re)installed`); if (shouldInstallServerPackages && shouldUninstallServerPackages) { this.log.info('Full packages reinstall is going to be performed'); } if (shouldUninstallServerPackages) { const silentUninstallPkg = async (pkgId) => { try { await this.adb.uninstallApk(pkgId); } catch (err) { this.log.info(`Cannot uninstall '${pkgId}': ${err.message}`); } }; await Promise.all(packagesInfo.map(({ appId }) => silentUninstallPkg(appId))); } if (shouldInstallServerPackages) { const installPkg = async (pkgPath) => { await this.adb.install(pkgPath, { noIncremental: true, replace: true, timeout: installTimeout, timeoutCapName: 'uiautomator2ServerInstallTimeout', }); }; await Promise.all(packagesInfo.map(({ appPath }) => installPkg(appPath))); } await this.verifyServicesAvailability(); } async startSession(caps) { await this.cleanupAutomationLeftovers(); if (caps.skipServerInstallation) { this.log.info(`'skipServerInstallation' is set. Attempting to use UIAutomator2 server from the device`); } else { this.log.info(`Starting UIAutomator2 server ${appium_uiautomator2_server_1.version}`); this.log.info(`Using UIAutomator2 server from '${appium_uiautomator2_server_1.SERVER_APK_PATH}' and test from '${appium_uiautomator2_server_1.TEST_APK_PATH}'`); } const timeout = caps.uiautomator2ServerLaunchTimeout || SERVER_LAUNCH_TIMEOUT_MS; const timer = new support_1.timing.Timer().start(); let retries = 0; const maxRetries = 2; const delayBetweenRetries = 3000; while (retries < maxRetries) { this.log.info(`Waiting up to ${timeout}ms for UiAutomator2 to be online...`); this.jwproxy.didInstrumentationExit = false; try { await this.stopInstrumentationProcess(); } catch { } await this.startInstrumentationProcess(); if (!this.jwproxy.didInstrumentationExit) { try { await (0, asyncbox_1.waitForCondition)(async () => { try { await this.jwproxy.command('/status', 'GET'); return true; } catch { // short circuit to retry or fail fast return this.jwproxy.didInstrumentationExit; } }, { waitMs: timeout, intervalMs: 1000, }); } catch { throw this.log.errorWithException(`The instrumentation process cannot be initialized within ${timeout}ms timeout. ` + 'Make sure the application under test does not crash and investigate the logcat output. ' + `You could also try to increase the value of 'uiautomator2ServerLaunchTimeout' capability`); } } if (!this.jwproxy.didInstrumentationExit) { break; } retries++; if (retries >= maxRetries) { throw this.log.errorWithException('The instrumentation process cannot be initialized. ' + 'Make sure the application under test does not crash and investigate the logcat output.'); } this.log.warn(`The instrumentation process has been unexpectedly terminated. ` + `Retrying UiAutomator2 startup (#${retries} of ${maxRetries - 1})`); await this.cleanupAutomationLeftovers(true); await (0, asyncbox_1.sleep)(delayBetweenRetries); } this.log.debug(`The initialization of the instrumentation process took ` + `${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); await this.jwproxy.command('/session', 'POST', { capabilities: { firstMatch: [caps], alwaysMatch: {}, }, }); } async deleteSession() { this.log.debug('Deleting UiAutomator2 server session'); try { await this.jwproxy.command('/', 'DELETE'); } catch (err) { this.log.warn(`Did not get the confirmation of UiAutomator2 server session deletion. ` + `Original error: ${err.message}`); } // Theoretically we could also force kill instumentation and server processes // without waiting for them to properly quit on their own. // This may cause unexpected error reports in device logs though. await this._waitForTermination(); try { await this.stopInstrumentationProcess(); } catch (err) { this.log.warn(`Could not stop the instrumentation process. Original error: ${err.message}`); } try { await Promise.all([ this.adb.forceStop(packages_1.SERVER_PACKAGE_ID), this.adb.forceStop(packages_1.SERVER_TEST_PACKAGE_ID), ]); } catch { } } async prepareServerPackage(appPath, appId) { const resultInfo = { installState: this.adb.APP_INSTALL_STATE.NOT_INSTALLED, appPath, appId, }; if (appId === packages_1.SERVER_TEST_PACKAGE_ID && (await this.adb.isAppInstalled(appId))) { // There is no point in getting the state for the test server, // since it does not contain any version info resultInfo.installState = this.adb.APP_INSTALL_STATE.SAME_VERSION_INSTALLED; } else if (appId === packages_1.SERVER_PACKAGE_ID) { resultInfo.installState = await this.adb.getApplicationInstallState(resultInfo.appPath, appId); } return resultInfo; } /** * Checks if server components must be installed from the device under test * in scope of the current driver session. * * For example, if one of servers on the device under test was newer than servers current UIA2 driver wants to * use for the session, the UIA2 driver should uninstall the installed ones in order to avoid * version mismatch between the UIA2 drier and servers on the device under test. * Also, if the device under test has missing servers, current UIA2 driver should uninstall all * servers once in order to install proper servers freshly. * * @param packagesInfo * @returns true if any of components is already installed and the other is not installed * or the installed one has a newer version. */ shouldUninstallServerPackages(packagesInfo = []) { const isAnyComponentInstalled = packagesInfo.some(({ installState }) => installState !== this.adb.APP_INSTALL_STATE.NOT_INSTALLED); const isAnyComponentNotInstalledOrNewer = packagesInfo.some(({ installState }) => [ this.adb.APP_INSTALL_STATE.NOT_INSTALLED, this.adb.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED, ].includes(installState)); return isAnyComponentInstalled && isAnyComponentNotInstalledOrNewer; } /** * Checks if server components should be installed on the device under test in scope of the current driver session. * * @param packagesInfo * @returns true if any of components is not installed or older than currently installed in order to * install or upgrade the servers on the device under test. */ shouldInstallServerPackages(packagesInfo = []) { return packagesInfo.some(({ installState }) => [ this.adb.APP_INSTALL_STATE.NOT_INSTALLED, this.adb.APP_INSTALL_STATE.OLDER_VERSION_INSTALLED, ].includes(installState)); } async verifyServicesAvailability() { this.log.debug(`Waiting up to ${SERVICES_LAUNCH_TIMEOUT_MS}ms for services to be available`); let isPmServiceAvailable = false; let pmOutput = ''; let pmError = null; try { await (0, asyncbox_1.waitForCondition)(async () => { if (!isPmServiceAvailable) { pmError = null; pmOutput = ''; try { pmOutput = await this.adb.shell(['pm', 'list', 'instrumentation']); } catch (e) { pmError = e; } if (pmOutput.includes('Could not access the Package Manager')) { pmError = new Error(`Problem running Package Manager: ${pmOutput}`); pmOutput = ''; // remove output, so it is not printed below } else if (pmOutput.includes(exports.INSTRUMENTATION_TARGET)) { pmOutput = ''; // remove output, so it is not printed below this.log.debug(`Instrumentation target '${exports.INSTRUMENTATION_TARGET}' is available`); isPmServiceAvailable = true; } else if (!pmError) { pmError = new Error('The instrumentation target is not listed by Package Manager'); } } return isPmServiceAvailable; }, { waitMs: SERVICES_LAUNCH_TIMEOUT_MS, intervalMs: 1000, }); } catch { const errorMessage = pmError?.message || 'Unknown error'; this.log.error(`Unable to find instrumentation target '${exports.INSTRUMENTATION_TARGET}': ${errorMessage}`); if (pmOutput) { this.log.debug('Available targets:'); for (const line of pmOutput.split('\n')) { this.log.debug(` ${line.replace('instrumentation:', '')}`); } } } } async startInstrumentationProcess() { const cmd = ['am', 'instrument', '-w']; if (this.disableWindowAnimation) { cmd.push('--no-window-animation'); } if (typeof this.disableSuppressAccessibilityService === 'boolean') { cmd.push('-e', 'DISABLE_SUPPRESS_ACCESSIBILITY_SERVICES', `${this.disableSuppressAccessibilityService}`); } // Disable Google analytics to prevent possible fatal exception cmd.push('-e', 'disableAnalytics', 'true'); cmd.push(exports.INSTRUMENTATION_TARGET); this.instrumentationProcess = this.adb.createSubProcess(['shell', ...cmd]); for (const streamName of ['stderr', 'stdout']) { this.instrumentationProcess.on(`line-${streamName}`, (line) => this.log.debug(`[Instrumentation] ${line}`)); } this.instrumentationProcess.once('exit', (code, signal) => { this.log.debug(`[Instrumentation] The process has exited with code ${code}, signal ${signal}`); this.jwproxy.didInstrumentationExit = true; }); await this.instrumentationProcess.start(0); } async stopInstrumentationProcess() { try { if (this.instrumentationProcess?.isRunning) { await this.instrumentationProcess.stop(); } } finally { this.instrumentationProcess?.removeAllListeners(); this.instrumentationProcess = null; } } async cleanupAutomationLeftovers(strictCleanup = false) { this.log.debug(`Performing ${strictCleanup ? 'strict' : 'shallow'} cleanup of automation leftovers`); const serverBase = `http://${this.host}:${this.systemPort}`; try { const { value } = (await (0, axios_1.default)({ url: `${serverBase}/sessions`, timeout: SERVER_REQUEST_TIMEOUT_MS, })).data; const activeSessionIds = value.map(({ id }) => id).filter(Boolean); if (activeSessionIds.length) { this.log.debug(`The following obsolete sessions are still running: ${activeSessionIds}`); this.log.debug(`Cleaning up ${support_1.util.pluralize('obsolete session', activeSessionIds.length, true)}`); await Promise.all(activeSessionIds.map((id) => axios_1.default.delete(`${serverBase}/session/${id}`, { timeout: SERVER_REQUEST_TIMEOUT_MS, }))); // Let the server to be properly terminated before continuing await this._waitForTermination(); } else { this.log.debug('No obsolete sessions have been detected'); } } catch (e) { this.log.debug(`No obsolete sessions have been detected (${e.message})`); } try { await Promise.all([ this.adb.forceStop(packages_1.SERVER_PACKAGE_ID), this.adb.forceStop(packages_1.SERVER_TEST_PACKAGE_ID), ]); } catch { } if (strictCleanup) { // https://github.com/appium/appium/issues/10749 try { await this.adb.killProcessesByName('uiautomator'); } catch { } } } /** * Blocks until UIA2 server stops running * or SERVER_SHUTDOWN_TIMEOUT_MS expires * * @returns {Promise<void>} */ async _waitForTermination() { try { await (0, asyncbox_1.waitForCondition)(async () => { try { return !(await this.adb.processExists(packages_1.SERVER_PACKAGE_ID)); } catch { return true; } }, { waitMs: SERVER_SHUTDOWN_TIMEOUT_MS, intervalMs: 300, }); } catch { this.log.warn(`The UIA2 server has not been terminated within ${SERVER_SHUTDOWN_TIMEOUT_MS}ms timeout. ` + `Continuing anyway`); } } } exports.UiAutomator2Server = UiAutomator2Server; //# sourceMappingURL=core.js.map