UNPKG

appium-adb

Version:

Android Debug Bridge interface

1,244 lines 56.3 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.getBuildToolsDirs = exports.getVersion = exports.EXEC_OUTPUT_FORMAT = exports.resetTelnetAuthToken = exports.getBinaryNameForOS = void 0; exports.getSdkBinaryPath = getSdkBinaryPath; exports.getBinaryFromSdkRoot = getBinaryFromSdkRoot; exports.getAndroidBinaryPath = getAndroidBinaryPath; exports.getBinaryFromPath = getBinaryFromPath; exports.getConnectedDevices = getConnectedDevices; exports.getDevicesWithRetry = getDevicesWithRetry; exports.reconnect = reconnect; exports.restartAdb = restartAdb; exports.killServer = killServer; exports.adbExecEmu = adbExecEmu; exports.adbExec = adbExec; exports.shell = shell; exports.createSubProcess = createSubProcess; exports.getAdbServerPort = getAdbServerPort; exports.getEmulatorPort = getEmulatorPort; exports.getPortFromEmulatorString = getPortFromEmulatorString; exports.getConnectedEmulators = getConnectedEmulators; exports.setEmulatorPort = setEmulatorPort; exports.setDeviceId = setDeviceId; exports.setDevice = setDevice; exports.getRunningAVD = getRunningAVD; exports.getRunningAVDWithRetry = getRunningAVDWithRetry; exports.killAllEmulators = killAllEmulators; exports.killEmulator = killEmulator; exports.launchAVD = launchAVD; exports.waitForEmulatorReady = waitForEmulatorReady; exports.waitForDevice = waitForDevice; exports.reboot = reboot; exports.changeUserPrivileges = changeUserPrivileges; exports.root = root; exports.unroot = unroot; exports.isRoot = isRoot; exports.installMitmCertificate = installMitmCertificate; exports.isMitmCertificateInstalled = isMitmCertificateInstalled; exports.shellChunks = shellChunks; exports.toAvdLocaleArgs = toAvdLocaleArgs; const path_1 = __importDefault(require("path")); const logger_js_1 = require("../logger.js"); const bluebird_1 = __importDefault(require("bluebird")); const support_1 = require("@appium/support"); const helpers_1 = require("../helpers"); const teen_process_1 = require("teen_process"); const asyncbox_1 = require("asyncbox"); const lodash_1 = __importDefault(require("lodash")); const semver = __importStar(require("semver")); const DEFAULT_ADB_REBOOT_RETRIES = 90; const LINKER_WARNING_REGEXP = /^WARNING: linker.+$/m; const ADB_RETRY_ERROR_PATTERNS = [ /protocol fault \(no status\)/i, /error: device ('.+' )?not found/i, /error: device still connecting/i, ]; const BINARY_VERSION_PATTERN = /^Version ([\d.]+)-(\d+)/m; const BRIDGE_VERSION_PATTERN = /^Android Debug Bridge version ([\d.]+)/m; const CERTS_ROOT = '/system/etc/security/cacerts'; const SDK_BINARY_ROOTS = [ 'platform-tools', 'emulator', ['cmdline-tools', 'latest', 'bin'], 'tools', ['tools', 'bin'], '.' // Allow custom sdkRoot to specify full folder path ]; const MIN_DELAY_ADB_API_LEVEL = 28; const REQUIRED_SERVICES = ['activity', 'package', 'mount']; const SUBSYSTEM_STATE_OK = 'Subsystem state: true'; const MAX_SHELL_BUFFER_LENGTH = 1000; /** * Retrieve full path to the given binary. * * @this {import('../adb.js').ADB} * @param {string} binaryName - The name of the binary. * @return {Promise<string>} Full path to the given binary including current SDK root. */ async function getSdkBinaryPath(binaryName) { return await this.getBinaryFromSdkRoot(binaryName); } /** * Retrieve full binary name for the current operating system. * * @param {string} binaryName - simple binary name, for example 'android'. * @return {string} Formatted binary name depending on the current platform, * for example, 'android.bat' on Windows. */ function _getBinaryNameForOS(binaryName) { if (!support_1.system.isWindows()) { return binaryName; } if (['android', 'apksigner', 'apkanalyzer'].includes(binaryName)) { return `${binaryName}.bat`; } if (!path_1.default.extname(binaryName)) { return `${binaryName}.exe`; } return binaryName; } exports.getBinaryNameForOS = lodash_1.default.memoize(_getBinaryNameForOS); /** * Retrieve full path to the given binary and caches it into `binaries` * property of the current ADB instance. * * @this {import('../adb.js').ADB} * @param {string} binaryName - Simple name of a binary file. * @return {Promise<string>} Full path to the given binary. The method tries * to enumerate all the known locations where the binary * might be located and stops the search as soon as the first * match is found on the local file system. * @throws {Error} If the binary with given name is not present at any * of known locations or Android SDK is not installed on the * local file system. */ async function getBinaryFromSdkRoot(binaryName) { if (( /** @type {import('./types').StringRecord} */(this.binaries))[binaryName]) { return ( /** @type {import('./types').StringRecord} */(this.binaries))[binaryName]; } const fullBinaryName = this.getBinaryNameForOS(binaryName); const binaryLocs = getSdkBinaryLocationCandidates( /** @type {string} */ (this.sdkRoot), fullBinaryName); // get subpaths for currently installed build tool directories let buildToolsDirs = await (0, exports.getBuildToolsDirs)(/** @type {string} */ (this.sdkRoot)); if (this.buildToolsVersion) { buildToolsDirs = buildToolsDirs .filter((x) => path_1.default.basename(x) === this.buildToolsVersion); if (lodash_1.default.isEmpty(buildToolsDirs)) { logger_js_1.log.info(`Found no build tools whose version matches to '${this.buildToolsVersion}'`); } else { logger_js_1.log.info(`Using build tools at '${buildToolsDirs}'`); } } binaryLocs.push(...(lodash_1.default.flatten(buildToolsDirs .map((dir) => [ path_1.default.resolve(dir, fullBinaryName), path_1.default.resolve(dir, 'lib', fullBinaryName), ])))); let binaryLoc = null; for (const loc of binaryLocs) { if (await support_1.fs.exists(loc)) { binaryLoc = loc; break; } } if (lodash_1.default.isNull(binaryLoc)) { throw new Error(`Could not find '${fullBinaryName}' in ${JSON.stringify(binaryLocs)}. ` + `Do you have Android Build Tools ${this.buildToolsVersion ? `v ${this.buildToolsVersion} ` : ''}` + `installed at '${this.sdkRoot}'?`); } logger_js_1.log.info(`Using '${fullBinaryName}' from '${binaryLoc}'`); ( /** @type {import('./types').StringRecord} */(this.binaries))[binaryName] = binaryLoc; return binaryLoc; } /** * Returns the Android binaries locations * * @param {string} sdkRoot The path to Android SDK root. * @param {string} fullBinaryName The name of full binary name. * @return {string[]} The list of SDK_BINARY_ROOTS paths * with sdkRoot and fullBinaryName. */ function getSdkBinaryLocationCandidates(sdkRoot, fullBinaryName) { return SDK_BINARY_ROOTS.map((x) => path_1.default.resolve(sdkRoot, ...(lodash_1.default.isArray(x) ? x : [x]), fullBinaryName)); } /** * Retrieve full path to the given binary. * This method does not have cache. * * @param {string} binaryName - Simple name of a binary file. * e.g. 'adb', 'android' * @return {Promise<string>} Full path to the given binary. The method tries * to enumerate all the known locations where the binary * might be located and stops the search as soon as the first * match is found on the local file system. * e.g. '/Path/To/Android/sdk/platform-tools/adb' * @throws {Error} If the binary with given name is not present at any * of known locations or Android SDK is not installed on the * local file system. */ async function getAndroidBinaryPath(binaryName) { const fullBinaryName = (0, exports.getBinaryNameForOS)(binaryName); const sdkRoot = (0, helpers_1.getSdkRootFromEnv)(); const binaryLocs = getSdkBinaryLocationCandidates(sdkRoot ?? '', fullBinaryName); for (const loc of binaryLocs) { if (await support_1.fs.exists(loc)) { return loc; } } throw new Error(`Could not find '${fullBinaryName}' in ${JSON.stringify(binaryLocs)}. ` + `Do you have Android Build Tools installed at '${sdkRoot}'?`); } /** * Retrieve full path to a binary file using the standard system lookup tool. * * @this {import('../adb.js').ADB} * @param {string} binaryName - The name of the binary. * @return {Promise<string>} Full path to the binary received from 'which'/'where' * output. * @throws {Error} If lookup tool returns non-zero return code. */ async function getBinaryFromPath(binaryName) { if (( /** @type {import('./types').StringRecord} */(this.binaries))[binaryName]) { return ( /** @type {import('./types').StringRecord} */(this.binaries))[binaryName]; } const fullBinaryName = this.getBinaryNameForOS(binaryName); try { const binaryLoc = await support_1.fs.which(fullBinaryName); logger_js_1.log.info(`Using '${fullBinaryName}' from '${binaryLoc}'`); ( /** @type {import('./types').StringRecord} */(this.binaries))[binaryName] = binaryLoc; return binaryLoc; } catch { throw new Error(`Could not find '${fullBinaryName}' in PATH. Please set the ANDROID_HOME ` + `or ANDROID_SDK_ROOT environment variables to the correct Android SDK root directory path.`); } } /** * Retrieve the list of devices visible to adb. * * @this {import('../adb.js').ADB} * @param {import('./types').ConnectedDevicesOptions} [opts={}] - Additional options mapping. * @return {Promise<import('./types').Device[]>} The list of devices or an empty list if * no devices are connected. * @throws {Error} If there was an error while listing devices. */ async function getConnectedDevices(opts = {}) { logger_js_1.log.debug('Getting connected devices'); const args = [...this.executable.defaultArgs, 'devices']; if (opts.verbose) { args.push('-l'); } let stdout; try { ({ stdout } = await (0, teen_process_1.exec)(this.executable.path, args)); } catch (e) { throw new Error(`Error while getting connected devices. Original error: ${e.message}`); } const listHeader = 'List of devices'; // expecting adb devices to return output as // List of devices attached // emulator-5554 device const startingIndex = stdout.indexOf(listHeader); if (startingIndex < 0) { throw new Error(`Unexpected output while trying to get devices: ${stdout}`); } // slicing output we care about stdout = stdout.slice(startingIndex); let excludedLines = [listHeader, 'adb server', '* daemon']; if (!this.allowOfflineDevices) { excludedLines.push('offline'); } const devices = stdout.split('\n') .map(lodash_1.default.trim) .filter((line) => line && !excludedLines.some((x) => line.includes(x))) .map((line) => { // state is "device", afaic const [udid, state, ...description] = line.split(/\s+/); const device = { udid, state }; if (opts.verbose) { for (const entry of description) { if (entry.includes(':')) { // each entry looks like key:value const [key, value] = entry.split(':'); device[key] = value; } } } return device; }); if (lodash_1.default.isEmpty(devices)) { logger_js_1.log.debug('No connected devices have been detected'); } else { logger_js_1.log.debug(`Connected devices: ${JSON.stringify(devices)}`); } return devices; } /** * Retrieve the list of devices visible to adb within the given timeout. * * @this {import('../adb.js').ADB} * @param {number} timeoutMs - The maximum number of milliseconds to get at least * one list item. * @return {Promise<import('./types').Device[]>} The list of connected devices. * @throws {Error} If no connected devices can be detected within the given timeout. */ async function getDevicesWithRetry(timeoutMs = 20000) { logger_js_1.log.debug('Trying to find connected Android devices'); try { let devices; await (0, asyncbox_1.waitForCondition)(async () => { try { devices = await this.getConnectedDevices(); if (devices.length) { return true; } logger_js_1.log.debug('Could not find online devices'); } catch (err) { logger_js_1.log.debug(err.stack); logger_js_1.log.warn(`Got an unexpected error while fetching connected devices list: ${err.message}`); } try { await this.reconnect(); } catch { await this.restartAdb(); } return false; }, { waitMs: timeoutMs, intervalMs: 200, }); return /** @type {any} */ (devices); } catch (e) { if (/Condition unmet/.test(e.message)) { throw new Error(`Could not find a connected Android device in ${timeoutMs}ms`); } else { throw e; } } } /** * Kick current connection from host/device side and make it reconnect * * @this {import('../adb.js').ADB} * @param {string} [target=offline] One of possible targets to reconnect: * offline, device or null * Providing `null` will cause reconnection to happen from the host side. * * @throws {Error} If either ADB version is too old and does not support this * command or there was a failure during reconnect. */ async function reconnect(target = 'offline') { logger_js_1.log.debug(`Reconnecting adb (target ${target})`); const args = ['reconnect']; if (target) { args.push(target); } try { await this.adbExec(args); } catch (e) { throw new Error(`Cannot reconnect adb. Original error: ${e.stderr || e.message}`); } } /** * Restart adb server, unless _this.suppressKillServer_ property is true. * * @this {import('../adb.js').ADB} */ async function restartAdb() { if (this.suppressKillServer) { logger_js_1.log.debug(`Not restarting abd since 'suppressKillServer' is on`); return; } logger_js_1.log.debug('Restarting adb'); try { await this.killServer(); await this.adbExec(['start-server']); } catch { logger_js_1.log.error(`Error killing ADB server, going to see if it's online anyway`); } } /** * Kill adb server. * @this {import('../adb.js').ADB} */ async function killServer() { logger_js_1.log.debug(`Killing adb server on port '${this.adbPort}'`); await this.adbExec(['kill-server'], { exclusive: true, }); } /** * Reset Telnet authentication token. * @see {@link http://tools.android.com/recent/emulator2516releasenotes} for more details. * * @this {import('../adb.js').ADB} * @returns {Promise<boolean>} If token reset was successful. */ exports.resetTelnetAuthToken = lodash_1.default.memoize(async function resetTelnetAuthToken() { // The methods is used to remove telnet auth token // const homeFolderPath = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; if (!homeFolderPath) { logger_js_1.log.warn(`Cannot find the path to user home folder. Ignoring resetting of emulator's telnet authentication token`); return false; } const dstPath = path_1.default.resolve(homeFolderPath, '.emulator_console_auth_token'); logger_js_1.log.debug(`Overriding ${dstPath} with an empty string to avoid telnet authentication for emulator commands`); try { await support_1.fs.writeFile(dstPath, ''); } catch (e) { logger_js_1.log.warn(`Error ${e.message} while resetting the content of ${dstPath}. Ignoring resetting of emulator's telnet authentication token`); return false; } return true; }); /** * Execute the given emulator command using _adb emu_ tool. * * @this {import('../adb.js').ADB} * @param {string[]} cmd - The array of rest command line parameters. */ async function adbExecEmu(cmd) { await this.verifyEmulatorConnected(); await this.resetTelnetAuthToken(); await this.adbExec(['emu', ...cmd]); } let isExecLocked = false; /** @type {{STDOUT: 'stdout', FULL: 'full'}} */ exports.EXEC_OUTPUT_FORMAT = Object.freeze({ STDOUT: 'stdout', FULL: 'full', }); /** * Execute the given adb command. * * @template {import('teen_process').TeenProcessExecOptions & import('./types').ShellExecOptions & import('./types').SpecialAdbExecOptions} TExecOpts * @this {import('../adb.js').ADB} * @param {string|string[]} cmd - The array of rest command line parameters * or a single string parameter. * @param {TExecOpts} [opts] Additional options mapping. See * {@link https://github.com/appium/node-teen_process} * for more details. * You can also set the additional `exclusive` param * to `true` that assures no other parallel adb commands * are going to be executed while the current one is running * You can set the `outputFormat` param to `stdout` to receive just the stdout * output (default) or `full` to receive the stdout and stderr response from a * command with a zero exit code * @return {Promise<TExecOpts extends import('./types').TFullOutputOption ? import('teen_process').TeenProcessExecResult : string>} * Command's stdout or an object containing stdout and stderr. * @throws {Error} If the command returned non-zero exit code. */ async function adbExec(cmd, opts) { if (!cmd) { throw new Error('You need to pass in a command to adbExec()'); } const optsCopy = lodash_1.default.cloneDeep(opts) ?? /** @type {TExecOpts} */ ({}); // setting default timeout for each command to prevent infinite wait. optsCopy.timeout = optsCopy.timeout || this.adbExecTimeout || helpers_1.DEFAULT_ADB_EXEC_TIMEOUT; optsCopy.timeoutCapName = optsCopy.timeoutCapName || 'adbExecTimeout'; // For error message const { outputFormat = this.EXEC_OUTPUT_FORMAT.STDOUT } = optsCopy; cmd = lodash_1.default.isArray(cmd) ? cmd : [cmd]; let adbRetried = false; const execFunc = async () => { try { const args = [...this.executable.defaultArgs, ...cmd]; logger_js_1.log.debug(`Running '${this.executable.path} ` + (args.find((arg) => /\s+/.test(arg)) ? support_1.util.quote(args) : args.join(' ')) + `'`); let { stdout, stderr } = await (0, teen_process_1.exec)(this.executable.path, args, optsCopy); // sometimes ADB prints out weird stdout warnings that we don't want // to include in any of the response data, so let's strip it out stdout = stdout.replace(LINKER_WARNING_REGEXP, '').trim(); return outputFormat === this.EXEC_OUTPUT_FORMAT.FULL ? { stdout, stderr } : stdout; } catch (e) { const errText = `${e.message}, ${e.stdout}, ${e.stderr}`; if (ADB_RETRY_ERROR_PATTERNS.some((p) => p.test(errText))) { logger_js_1.log.info(`Error sending command, reconnecting device and retrying: ${cmd}`); await (0, asyncbox_1.sleep)(1000); await this.getDevicesWithRetry(); // try again one time if (adbRetried) { adbRetried = true; return await execFunc(); } } if (e.code === 0 && e.stdout) { return e.stdout.replace(LINKER_WARNING_REGEXP, '').trim(); } if (lodash_1.default.isNull(e.code)) { e.message = `Error executing adbExec. Original error: '${e.message}'. ` + `Try to increase the ${optsCopy.timeout}ms adb execution timeout ` + `represented by '${optsCopy.timeoutCapName}' capability`; } else { e.message = `Error executing adbExec. Original error: '${e.message}'; ` + `Command output: ${e.stderr || e.stdout || '<empty>'}`; } throw e; } }; if (isExecLocked) { logger_js_1.log.debug('Waiting until the other exclusive ADB command is completed'); await (0, asyncbox_1.waitForCondition)(() => !isExecLocked, { waitMs: Number.MAX_SAFE_INTEGER, intervalMs: 10, }); logger_js_1.log.debug('Continuing with the current ADB command'); } if (optsCopy.exclusive) { isExecLocked = true; } try { return await execFunc(); } finally { if (optsCopy.exclusive) { isExecLocked = false; } } } /** * Execute the given command using _adb shell_ prefix. * * @this {import('../adb.js').ADB} * @template {import('./types').ShellExecOptions} TShellExecOpts * @param {string|string[]} cmd - The array of rest command line parameters or a single * string parameter. * @param {TShellExecOpts} [opts] - Additional options mapping. * @return {Promise<TShellExecOpts extends import('./types').TFullOutputOption ? import('teen_process').TeenProcessExecResult : string>} * Command's stdout. * @throws {Error} If the command returned non-zero exit code. */ async function shell(cmd, opts) { const { privileged, } = opts ?? /** @type {TShellExecOpts} */ ({}); const cmdArr = lodash_1.default.isArray(cmd) ? cmd : [cmd]; const fullCmd = ['shell']; if (privileged) { logger_js_1.log.info(`'adb shell ${support_1.util.quote(cmdArr)}' requires root access`); if (await this.isRoot()) { logger_js_1.log.info('The device already had root access'); fullCmd.push(...cmdArr); } else { fullCmd.push('su', 'root', support_1.util.quote(cmdArr)); } } else { fullCmd.push(...cmdArr); } return await this.adbExec(fullCmd, opts); } /** * * @this {import('../adb.js').ADB} * @param {string[]} [args=[]] * @returns {import('teen_process').SubProcess} */ function createSubProcess(args = []) { // add the default arguments const finalArgs = [...this.executable.defaultArgs, ...args]; logger_js_1.log.debug(`Creating ADB subprocess with args: ${JSON.stringify(finalArgs)}`); return new teen_process_1.SubProcess(this.getAdbPath(), finalArgs); } /** * Retrieve the current adb port. * @todo can probably deprecate this now that the logic is just to read this.adbPort * * @this {import('../adb.js').ADB} * @return {number} The current adb port number. */ function getAdbServerPort() { return /** @type {number} */ (this.adbPort); } /** * Retrieve the current emulator port from _adb devives_ output. * * @this {import('../adb.js').ADB} * @return {Promise<number>} The current emulator port. * @throws {Error} If there are no connected devices. */ async function getEmulatorPort() { logger_js_1.log.debug('Getting running emulator port'); if (this.emulatorPort !== null) { return /** @type {number} */ (this.emulatorPort); } try { let devices = await this.getConnectedDevices(); let port = this.getPortFromEmulatorString(devices[0].udid); if (port) { return port; } else { throw new Error(`Emulator port not found`); } } catch (e) { throw new Error(`No devices connected. Original error: ${e.message}`); } } /** * Retrieve the current emulator port by parsing emulator name string. * * @this {import('../adb.js').ADB} * @param {string} emStr - Emulator name string. * @return {number|false} Either the current emulator port or * _false_ if port number cannot be parsed. */ function getPortFromEmulatorString(emStr) { let portPattern = /emulator-(\d+)/; if (portPattern.test(emStr)) { return parseInt(( /** @type {RegExpExecArray} */(portPattern.exec(emStr)))[1], 10); } return false; } /** * Retrieve the list of currently connected emulators. * * @this {import('../adb.js').ADB} * @param {import('./types').ConnectedDevicesOptions} [opts={}] - Additional options mapping. * @return {Promise<import('./types').Device[]>} The list of connected devices. */ async function getConnectedEmulators(opts = {}) { logger_js_1.log.debug('Getting connected emulators'); try { let devices = await this.getConnectedDevices(opts); let emulators = []; for (let device of devices) { let port = this.getPortFromEmulatorString(device.udid); if (port) { device.port = port; emulators.push(device); } } logger_js_1.log.debug(`${support_1.util.pluralize('emulator', emulators.length, true)} connected`); return emulators; } catch (e) { throw new Error(`Error getting emulators. Original error: ${e.message}`); } } /** * Set _emulatorPort_ property of the current class. * * @this {import('../adb.js').ADB} * @param {number} emPort - The emulator port to be set. */ function setEmulatorPort(emPort) { this.emulatorPort = emPort; } /** * Set the identifier of the current device (_this.curDeviceId_). * * @this {import('../adb.js').ADB} * @param {string} deviceId - The device identifier. */ function setDeviceId(deviceId) { logger_js_1.log.debug(`Setting device id to ${deviceId}`); this.curDeviceId = deviceId; let argsHasDevice = this.executable.defaultArgs.indexOf('-s'); if (argsHasDevice !== -1) { // remove the old device id from the arguments this.executable.defaultArgs.splice(argsHasDevice, 2); } this.executable.defaultArgs.push('-s', deviceId); } /** * Set the the current device object. * * @this {import('../adb.js').ADB} * @param {import('./types').Device} deviceObj - The device object to be set. */ function setDevice(deviceObj) { const deviceId = deviceObj.udid; const emPort = this.getPortFromEmulatorString(deviceId); if (lodash_1.default.isNumber(emPort)) { this.setEmulatorPort(emPort); } this.setDeviceId(deviceId); } /** * Get the object for the currently running emulator. * !!! This method has a side effect - it implicitly changes the * `deviceId` (only if AVD with a matching name is found) * and `emulatorPort` instance properties. * * @this {import('../adb.js').ADB} * @param {string} avdName - Emulator name. * @return {Promise<import('./types').Device|null>} Currently running emulator or _null_. */ async function getRunningAVD(avdName) { logger_js_1.log.debug(`Trying to find '${avdName}' emulator`); try { const emulators = await this.getConnectedEmulators(); for (const emulator of emulators) { if (lodash_1.default.isNumber(emulator.port)) { this.setEmulatorPort(emulator.port); } const runningAVDName = await this.execEmuConsoleCommand(['avd', 'name'], { port: emulator.port, execTimeout: 5000, connTimeout: 1000, }); if (lodash_1.default.toLower(avdName) === lodash_1.default.toLower(runningAVDName.trim())) { logger_js_1.log.debug(`Found emulator '${avdName}' on port ${emulator.port}`); this.setDeviceId(emulator.udid); return emulator; } } logger_js_1.log.debug(`Emulator '${avdName}' not running`); return null; } catch (e) { throw new Error(`Error getting AVD. Original error: ${e.message}`); } } /** * Get the object for the currently running emulator. * * @this {import('../adb.js').ADB} * @param {string} avdName - Emulator name. * @param {number} [timeoutMs=20000] - The maximum number of milliseconds * to wait until at least one running AVD object * is detected. * @return {Promise<import('./types').Device|null>} Currently running emulator or _null_. * @throws {Error} If no device has been detected within the timeout. */ async function getRunningAVDWithRetry(avdName, timeoutMs = 20000) { try { return /** @type {import('./types').Device|null} */ (await (0, asyncbox_1.waitForCondition)(async () => { try { return await this.getRunningAVD(avdName.replace('@', '')); } catch (e) { logger_js_1.log.debug(e.message); return false; } }, { waitMs: timeoutMs, intervalMs: 1000, })); } catch (e) { throw new Error(`Error getting AVD with retry. Original error: ${e.message}`); } } /** * Shutdown all running emulators by killing their processes. * * @this {import('../adb.js').ADB} * @throws {Error} If killing tool returned non-zero return code. */ async function killAllEmulators() { let cmd, args; if (support_1.system.isWindows()) { cmd = 'TASKKILL'; args = ['TASKKILL', '/IM', 'emulator.exe']; } else { cmd = '/usr/bin/killall'; args = ['-m', 'emulator*']; } try { await (0, teen_process_1.exec)(cmd, args); } catch (e) { throw new Error(`Error killing emulators. Original error: ${e.message}`); } } /** * Kill emulator with the given name. No error * is thrown is given avd does not exist/is not running. * * @this {import('../adb.js').ADB} * @param {string?} [avdName=null] - The name of the emulator to be killed. If empty, * the current emulator will be killed. * @param {number} [timeout=60000] - The amount of time to wait before throwing * an exception about unsuccessful killing * @return {Promise<boolean>} - True if the emulator was killed, false otherwise. * @throws {Error} if there was a failure by killing the emulator */ async function killEmulator(avdName = null, timeout = 60000) { if (support_1.util.hasValue(avdName)) { logger_js_1.log.debug(`Killing avd '${avdName}'`); const device = await this.getRunningAVD(avdName); if (!device) { logger_js_1.log.info(`No avd with name '${avdName}' running. Skipping kill step.`); return false; } } else { // killing the current avd logger_js_1.log.debug(`Killing avd with id '${this.curDeviceId}'`); if (!await this.isEmulatorConnected()) { logger_js_1.log.debug(`Emulator with id '${this.curDeviceId}' not connected. Skipping kill step`); return false; } } await this.adbExec(['emu', 'kill']); logger_js_1.log.debug(`Waiting up to ${timeout}ms until the emulator '${avdName ? avdName : this.curDeviceId}' is killed`); try { await (0, asyncbox_1.waitForCondition)(async () => { try { return support_1.util.hasValue(avdName) ? !await this.getRunningAVD(avdName) : !await this.isEmulatorConnected(); } catch { } return false; }, { waitMs: timeout, intervalMs: 2000, }); } catch { throw new Error(`The emulator '${avdName ? avdName : this.curDeviceId}' is still running after being killed ${timeout}ms ago`); } logger_js_1.log.info(`Successfully killed the '${avdName ? avdName : this.curDeviceId}' emulator`); return true; } /** * Start an emulator with given parameters and wait until it is fully started. * * @this {import('../adb.js').ADB} * @param {string} avdName - The name of an existing emulator. * @param {import('./types').AvdLaunchOptions} [opts={}] * @returns {Promise<SubProcess>} Emulator subprocess instance * @throws {Error} If the emulator fails to start within the given timeout. */ async function launchAVD(avdName, opts = {}) { const { args = [], env = {}, language, country, launchTimeout = 60000, readyTimeout = 60000, retryTimes = 1, } = opts; logger_js_1.log.debug(`Launching Emulator with AVD ${avdName}, launchTimeout ` + `${launchTimeout}ms and readyTimeout ${readyTimeout}ms`); const emulatorBinaryPath = await this.getSdkBinaryPath('emulator'); if (avdName[0] === '@') { avdName = avdName.substr(1); } await this.checkAvdExist(avdName); /** @type {string[]} */ const launchArgs = ['-avd', avdName]; launchArgs.push(...(toAvdLocaleArgs(language ?? null, country ?? null))); let isDelayAdbFeatureEnabled = false; if (this.allowDelayAdb) { const { revision } = await this.getEmuVersionInfo(); if (revision && support_1.util.compareVersions(revision, '>=', '29.0.7')) { // https://androidstudio.googleblog.com/2019/05/emulator-2907-canary.html try { const { target } = await this.getEmuImageProperties(avdName); const apiMatch = /\d+/.exec(target); // https://issuetracker.google.com/issues/142533355 if (apiMatch && parseInt(apiMatch[0], 10) >= MIN_DELAY_ADB_API_LEVEL) { launchArgs.push('-delay-adb'); isDelayAdbFeatureEnabled = true; } else { throw new Error(`The actual image API version is below ${MIN_DELAY_ADB_API_LEVEL}`); } } catch (e) { logger_js_1.log.info(`The -delay-adb emulator startup detection feature will not be enabled. ` + `Original error: ${e.message}`); } } } else { logger_js_1.log.info('The -delay-adb emulator startup detection feature has been explicitly disabled'); } if (!lodash_1.default.isEmpty(args)) { launchArgs.push(...(lodash_1.default.isArray(args) ? args : /** @type {string[]} */ (support_1.util.shellParse(`${args}`)))); } logger_js_1.log.debug(`Running '${emulatorBinaryPath}' with args: ${support_1.util.quote(launchArgs)}`); if (!lodash_1.default.isEmpty(env)) { logger_js_1.log.debug(`Customized emulator environment: ${JSON.stringify(env)}`); } const proc = new teen_process_1.SubProcess(emulatorBinaryPath, launchArgs, { env: Object.assign({}, process.env, env), }); await proc.start(0); for (const streamName of ['stderr', 'stdout']) { proc.on(`line-${streamName}`, (line) => logger_js_1.log.debug(`[AVD OUTPUT] ${line}`)); } proc.on('die', (code, signal) => { logger_js_1.log.warn(`Emulator avd ${avdName} exited with code ${code}${signal ? `, signal ${signal}` : ''}`); }); await (0, asyncbox_1.retry)(retryTimes, async () => await this.getRunningAVDWithRetry(avdName, launchTimeout)); // At this point we have deviceId already assigned const timer = new support_1.timing.Timer().start(); if (isDelayAdbFeatureEnabled) { try { await this.adbExec(['wait-for-device'], { timeout: readyTimeout }); } catch (e) { throw new Error(`'${avdName}' Emulator has failed to boot: ${e.stderr || e.message}`); } } await this.waitForEmulatorReady(Math.trunc(readyTimeout - timer.getDuration().asMilliSeconds)); return proc; } /** * Get the adb version. The result of this method is cached. * * @this {import('../adb.js').ADB} * @return {Promise<import('./types').Version>} * @throws {Error} If it is not possible to parse adb binary version. */ exports.getVersion = lodash_1.default.memoize(async function getVersion() { let stdout; try { stdout = await this.adbExec('version'); } catch (e) { throw new Error(`Error getting adb version: ${e.stderr || e.message}`); } const result = {}; const binaryVersionMatch = BINARY_VERSION_PATTERN.exec(stdout); if (binaryVersionMatch) { result.binary = { version: semver.coerce(binaryVersionMatch[1]), build: parseInt(binaryVersionMatch[2], 10), }; } const bridgeVersionMatch = BRIDGE_VERSION_PATTERN.exec(stdout); if (bridgeVersionMatch) { result.bridge = { version: semver.coerce(bridgeVersionMatch[1]), }; } return result; }); /** * Check if the current emulator is ready to accept further commands (booting completed). * * @this {import('../adb.js').ADB} * @param {number} [timeoutMs=20000] - The maximum number of milliseconds to wait. * @returns {Promise<void>} * @throws {Error} If the emulator is not ready within the given timeout. */ async function waitForEmulatorReady(timeoutMs = 20000) { logger_js_1.log.debug(`Waiting up to ${timeoutMs}ms for the emulator to be ready`); if (await this.getApiLevel() >= 31) { /** @type {string|undefined} */ let state; try { await (0, asyncbox_1.waitForCondition)(async () => { try { state = await this.shell([ 'cmd', 'reboot_readiness', 'check-subsystems-state', '--list-blocking' ]); } catch (err) { // https://github.com/appium/appium/issues/18717 state = err.stdout || err.stderr; } if (lodash_1.default.includes(state, SUBSYSTEM_STATE_OK)) { return true; } logger_js_1.log.debug(`Waiting for emulator startup. Intermediate state: ${state}`); return false; }, { waitMs: timeoutMs, intervalMs: 1000, }); } catch { throw new Error(`Emulator is not ready within ${timeoutMs}ms${state ? ('. Reason: ' + state) : ''}`); } return; } /** @type {RegExp[]} */ const requiredServicesRe = REQUIRED_SERVICES.map((name) => new RegExp(`\\b${name}:`)); let services; try { await (0, asyncbox_1.waitForCondition)(async () => { try { services = await this.shell(['service', 'list']); return requiredServicesRe.every((pattern) => pattern.test(services)); } catch (err) { logger_js_1.log.debug(`Waiting for emulator startup. Intermediate error: ${err.message}`); return false; } }, { waitMs: timeoutMs, intervalMs: 3000, }); } catch { if (services) { logger_js_1.log.debug(`Recently listed services:\n${services}`); } const missingServices = lodash_1.default.zip(REQUIRED_SERVICES, requiredServicesRe) .filter(([, pattern]) => !( /** @type {RegExp} */(pattern)).test(services)) .map(([name]) => name); throw new Error(`Emulator is not ready within ${timeoutMs}ms ` + `(${missingServices} service${missingServices.length === 1 ? ' is' : 's are'} not running)`); } } /** * Check if the current device is ready to accept further commands (booting completed). * * @this {import('../adb.js').ADB} * @param {number} [appDeviceReadyTimeout=30] - The maximum number of seconds to wait. * @throws {Error} If the device is not ready within the given timeout. */ async function waitForDevice(appDeviceReadyTimeout = 30) { this.appDeviceReadyTimeout = appDeviceReadyTimeout; const retries = 3; const timeout = parseInt(`${this.appDeviceReadyTimeout}`, 10) * 1000 / retries; await (0, asyncbox_1.retry)(retries, async () => { try { await this.adbExec('wait-for-device', { timeout }); await this.ping(); } catch (e) { try { await this.reconnect(); } catch { await this.restartAdb(); } await this.getConnectedDevices(); throw new Error(`Error waiting for the device to be available. Original error: '${e.message}'`); } }); } /** * Reboot the current device and wait until it is completed. * * @this {import('../adb.js').ADB} * @param {number} [retries=DEFAULT_ADB_REBOOT_RETRIES] - The maximum number of reboot retries. * @throws {Error} If the device failed to reboot and number of retries is exceeded. */ async function reboot(retries = DEFAULT_ADB_REBOOT_RETRIES) { // Get root access so we can run the next shell commands which require root access const { wasAlreadyRooted } = await this.root(); try { // Stop and re-start the device await this.shell(['stop']); await bluebird_1.default.delay(2000); // let the emu finish stopping; await this.setDeviceProperty('sys.boot_completed', '0', { privileged: false // no need to set privileged true because device already rooted }); await this.shell(['start']); } catch (e) { const { message } = e; // provide a helpful error message if the reason reboot failed was because ADB couldn't gain root access if (message.includes('must be root')) { throw new Error(`Could not reboot device. Rebooting requires root access and ` + `attempt to get root access on device failed with error: '${message}'`); } throw e; } finally { // Return root state to what it was before if (!wasAlreadyRooted) { await this.unroot(); } } const timer = new support_1.timing.Timer().start(); await (0, asyncbox_1.retryInterval)(retries, 1000, async () => { if ((await this.getDeviceProperty('sys.boot_completed')) === '1') { return; } const msg = `Reboot is not completed after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`; // we don't want the stack trace logger_js_1.log.debug(msg); throw new Error(msg); }); } /** * Switch adb server root privileges. * * @this {import('../adb.js').ADB} * @param {boolean} isElevated - Should we elevate to to root or unroot? (default true) * @return {Promise<import('./types').RootResult>} */ async function changeUserPrivileges(isElevated) { const cmd = isElevated ? 'root' : 'unroot'; const retryIfOffline = async (cmdFunc) => { try { return await cmdFunc(); } catch (err) { // Check the output of the stdErr to see if there's any clues that show that the device went offline // and if it did go offline, restart ADB if (['closed', 'device offline', 'timeout expired'] .some((x) => (err.stderr || '').toLowerCase().includes(x))) { logger_js_1.log.warn(`Attempt to ${cmd} caused ADB to think the device went offline`); try { await this.reconnect(); } catch { await this.restartAdb(); } return await cmdFunc(); } else { throw err; } } }; // If it's already rooted, our job is done. No need to root it again. const isRoot = await retryIfOffline(async () => await this.isRoot()); if ((isRoot && isElevated) || (!isRoot && !isElevated)) { return { isSuccessful: true, wasAlreadyRooted: isRoot }; } let wasAlreadyRooted = isRoot; try { const { stdout } = await retryIfOffline(async () => await this.adbExec([cmd])); logger_js_1.log.debug(stdout); // on real devices in some situations we get an error in the stdout if (stdout) { if (stdout.includes('adbd cannot run as root')) { return { isSuccessful: false, wasAlreadyRooted }; } // if the device was already rooted, return that in the result if (stdout.includes('already running as root')) { wasAlreadyRooted = true; } } return { isSuccessful: true, wasAlreadyRooted }; } catch (err) { const { stderr = '', message } = err; logger_js_1.log.warn(`Unable to ${cmd} adb daemon. Original error: '${message}'. Stderr: '${stderr}'. Continuing.`); return { isSuccessful: false, wasAlreadyRooted }; } } /** * Switch adb server to root mode * * @this {import('../adb.js').ADB} * @return {Promise<import('./types').RootResult>} */ async function root() { return await this.changeUserPrivileges(true); } /** * Switch adb server to non-root mode. * * @this {import('../adb.js').ADB} * @return {Promise<import('./types').RootResult>} */ async function unroot() { return await this.changeUserPrivileges(false); } /** * Checks whether the current user is root * * @this {import('../adb.js').ADB} * @return {Promise<boolean>} True if the user is root * @throws {Error} if there was an error while identifying * the user. */ async function isRoot() { return (await this.shell(['whoami'])).trim() === 'root'; } /** * Installs the given certificate on a rooted real device or * an emulator. The emulator must be executed with `-writable-system` * command line option and adb daemon should be running in root * mode for this method to work properly. The method also requires * openssl tool to be available on the destination system. * Read https://github.com/appium/appium/issues/10964 * for more details on this topic * * @this {import('../adb.js').ADB} * @param {Buffer|string} cert - base64-decoded content of the actual certificate * represented as a string or a buffer * @throws {Error} If openssl tool is not available on the destination system * or if there was an error while installing the certificate */ async function installMitmCertificate(cert) { const openSsl = await getOpenSslForOs(); const tmpRoot = await support_1.tempDir.openDir(); try { const srcCert = path_1.default.resolve(tmpRoot, 'source.cer'); await support_1.fs.writeFile(srcCert, Buffer.isBuffer(cert) ? cert : Buffer.from(cert, 'base64')); const { stdout } = await (0, teen_process_1.exec)(openSsl, ['x509', '-noout', '-hash', '-in', srcCert]); const certHash = stdout.trim(); logger_js_1.log.debug(`Got certificate hash: ${certHash}`); logger_js_1.log.debug('Preparing certificate content'); const { stdout: stdoutBuff1 } = await (0, teen_process_1.exec)(openSsl, ['x509', '-in', srcCert], { isBuffer: true }); const { stdout: stdoutBuff2 } = await (0, teen_process_1.exec)(openSsl, [ 'x509', '-in', srcCert, '-text', '-fingerprint', '-noout' ], { isBuffer: true }); const dstCertContent = Buffer.concat([stdoutBuff1, stdoutBuff2]); const dstCert = path_1.default.resolve(tmpRoot, `${certHash}.0`); await support_1.fs.writeFile(dstCert, dstCertContent); logger_js_1.log.debug('Remounting /system in rw mode'); // Sometimes emulator reboot is still not fully finished on this stage, so retry await (0, asyncbox_1.retryInterval)(5, 2000, async () => await this.adbExec(['remount'])); logger_js_1.log.debug(`Uploading the generated certificate from '${dstCert}' to '${CERTS_ROOT}'`); await this.push(dstCert, CERTS_ROOT); logger_js_1.log.debug('Remounting /system to confirm changes'); await this.adbExec(['remount']); } catch (err) { throw new Error(`Cannot inject the custom certificate. ` + `Is the certificate properly encoded into base64-string? ` + `Do you have root permissions on the device? ` + `Original error: ${err.message}`); } finally { await support_1.fs.rimraf(tmpRoot); } } /** * Verifies if the given root certificate is already installed on the device. * * @this {import('../adb.js').ADB} * @param {Buffer|string} cert - base64-decoded content of the actual certificate * represented as a string or a