UNPKG

appium-adb

Version:

Android Debug Bridge interface

1,302 lines 52.6 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 node_path_1 = __importDefault(require("node:path")); const logger_1 = require("../logger"); 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', 'window']; const MAX_SHELL_BUFFER_LENGTH = 1000; // Private methods (defined early as they're used by public methods) /** * Retrieve full binary name for the current operating system. * * @param binaryName - The name of the binary * @returns The binary name with appropriate extension for the current OS */ function _getBinaryNameForOS(binaryName) { if (!support_1.system.isWindows()) { return binaryName; } if (['android', 'apksigner', 'apkanalyzer'].includes(binaryName)) { return `${binaryName}.bat`; } if (!node_path_1.default.extname(binaryName)) { return `${binaryName}.exe`; } return binaryName; } /** * Returns the Android binaries locations * * @param sdkRoot - The Android SDK root directory path * @param fullBinaryName - The full name of the binary (with extension) * @returns Array of possible binary location paths */ function getSdkBinaryLocationCandidates(sdkRoot, fullBinaryName) { return SDK_BINARY_ROOTS.map((x) => node_path_1.default.resolve(sdkRoot, ...(lodash_1.default.isArray(x) ? x : [x]), fullBinaryName)); } /** * Get the path to the openssl binary for the current operating system * * @returns The full path to the openssl binary * @throws {Error} If openssl is not found in PATH */ async function getOpenSslForOs() { const binaryName = `openssl${support_1.system.isWindows() ? '.exe' : ''}`; try { return await support_1.fs.which(binaryName); } catch { throw new Error('The openssl tool must be installed on the system and available on the path'); } } // Public methods /** * Retrieve full path to the given binary. * * @param binaryName - The name of the binary * @returns The full path to the binary */ async function getSdkBinaryPath(binaryName) { return await this.getBinaryFromSdkRoot(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. * * @param binaryName - The name of the binary * @returns The full path to the binary * @throws {Error} If SDK root is not set or binary cannot be found */ async function getBinaryFromSdkRoot(binaryName) { if (this.binaries?.[binaryName]) { return this.binaries[binaryName]; } const fullBinaryName = this.getBinaryNameForOS(binaryName); if (!this.sdkRoot) { throw new Error('SDK root is not set'); } const binaryLocs = getSdkBinaryLocationCandidates(this.sdkRoot, fullBinaryName); // get subpaths for currently installed build tool directories let buildToolsDirs = await (0, exports.getBuildToolsDirs)(this.sdkRoot); if (this.buildToolsVersion) { buildToolsDirs = buildToolsDirs.filter((x) => node_path_1.default.basename(x) === this.buildToolsVersion); if (lodash_1.default.isEmpty(buildToolsDirs)) { logger_1.log.info(`Found no build tools whose version matches to '${this.buildToolsVersion}'`); } else { logger_1.log.info(`Using build tools at '${buildToolsDirs}'`); } } binaryLocs.push(...lodash_1.default.flatten(buildToolsDirs.map((dir) => [ node_path_1.default.resolve(dir, fullBinaryName), node_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_1.log.info(`Using '${fullBinaryName}' from '${binaryLoc}'`); if (!this.binaries) { this.binaries = {}; } this.binaries[binaryName] = binaryLoc; return binaryLoc; } /** * Retrieve full path to the given binary. * This method does not have cache. * * @param binaryName - The name of the binary * @returns The full path to the binary * @throws {Error} If binary cannot be found in the Android SDK */ 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. * * @param binaryName - The name of the binary * @returns The full path to the binary * @throws {Error} If binary cannot be found in PATH */ async function getBinaryFromPath(binaryName) { if (this.binaries?.[binaryName]) { return this.binaries[binaryName]; } const fullBinaryName = this.getBinaryNameForOS(binaryName); try { const binaryLoc = await support_1.fs.which(fullBinaryName); logger_1.log.info(`Using '${fullBinaryName}' from '${binaryLoc}'`); if (!this.binaries) { this.binaries = {}; } 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. * * @param opts - Options for device retrieval * @returns Array of connected devices * @throws {Error} If adb devices command fails or returns unexpected output */ async function getConnectedDevices(opts = {}) { logger_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) { const error = e; throw new Error(`Error while getting connected devices. Original error: ${error.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); const 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_1.log.debug('No connected devices have been detected'); } else { logger_1.log.debug(`Connected devices: ${JSON.stringify(devices)}`); } return devices; } /** * Retrieve the list of devices visible to adb within the given timeout. * * @param timeoutMs - Maximum time to wait for devices (default: 20000ms) * @returns Array of connected devices * @throws {Error} If no devices are found within the timeout period */ async function getDevicesWithRetry(timeoutMs = 20000) { logger_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_1.log.debug('Could not find online devices'); } catch (err) { const error = err; logger_1.log.debug(error.stack); logger_1.log.warn(`Got an unexpected error while fetching connected devices list: ${error.message}`); } try { await this.reconnect(); } catch { await this.restartAdb(); } return false; }, { waitMs: timeoutMs, intervalMs: 200, }); return devices; } catch (e) { const error = e; if (/Condition unmet/.test(error.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 * * @param target - The target to reconnect (default: 'offline') * @throws {Error} If reconnect command fails */ async function reconnect(target = 'offline') { logger_1.log.debug(`Reconnecting adb (target ${target})`); const args = ['reconnect']; if (target) { args.push(target); } try { await this.adbExec(args); } catch (e) { const error = e; throw new Error(`Cannot reconnect adb. Original error: ${error.stderr || error.message}`); } } /** * Restart adb server, unless _this.suppressKillServer_ property is true. */ async function restartAdb() { if (this.suppressKillServer) { logger_1.log.debug(`Not restarting abd since 'suppressKillServer' is on`); return; } logger_1.log.debug('Restarting adb'); try { await this.killServer(); await this.adbExec(['start-server']); } catch { logger_1.log.error(`Error killing ADB server, going to see if it's online anyway`); } } /** * Kill adb server. */ async function killServer() { logger_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. * * @returns True if token was reset successfully, false otherwise */ 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_1.log.warn(`Cannot find the path to user home folder. Ignoring resetting of emulator's telnet authentication token`); return false; } const dstPath = node_path_1.default.resolve(homeFolderPath, '.emulator_console_auth_token'); logger_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) { const error = e; logger_1.log.warn(`Error ${error.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. * * @param cmd - Array of command arguments * @throws {Error} If emulator is not connected or command execution fails */ async function adbExecEmu(cmd) { await this.verifyEmulatorConnected(); await this.resetTelnetAuthToken(); await this.adbExec(['emu', ...cmd]); } let isExecLocked = false; exports.EXEC_OUTPUT_FORMAT = { STDOUT: 'stdout', FULL: 'full', }; /** * Execute the given adb command. * * @param cmd - Command string or array of command arguments * @param opts - Execution options * @returns Command output (string or ExecResult depending on outputFormat) * @throws {Error} If command execution fails or timeout is exceeded */ 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 ?? {}); // 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_1.log.debug(`Running '${this.executable.path} ` + (args.find((arg) => /\s+/.test(arg)) ? support_1.util.quote(args) : args.join(' ')) + `'`); const { stdout: rawStdout, 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 const stdout = rawStdout.replace(LINKER_WARNING_REGEXP, '').trim(); return outputFormat === this.EXEC_OUTPUT_FORMAT.FULL ? { stdout, stderr } : stdout; } catch (e) { const error = e; const errText = `${error.message}, ${error.stdout}, ${error.stderr}`; if (ADB_RETRY_ERROR_PATTERNS.some((p) => p.test(errText))) { logger_1.log.info(`Error sending command, reconnecting device and retrying: ${cmd}`); await this.getDevicesWithRetry(); // try again one time if (!adbRetried) { adbRetried = true; return await execFunc(); } } if (error.code === 0 && error.stdout) { return error.stdout.replace(LINKER_WARNING_REGEXP, '').trim(); } if (lodash_1.default.isNull(error.code)) { error.message = `Error executing adbExec. Original error: '${error.message}'. ` + `Try to increase the ${optsCopy.timeout}ms adb execution timeout ` + `represented by '${optsCopy.timeoutCapName}' capability`; } else { error.message = `Error executing adbExec. Original error: '${error.message}'; ` + `Command output: ${error.stderr || error.stdout || '<empty>'}`; } throw error; } }; if (isExecLocked) { logger_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_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. * * @param cmd - Command string or array of command arguments * @param opts - Execution options * @returns Command output (string or ExecResult depending on outputFormat) * @throws {Error} If command execution fails */ async function shell(cmd, opts) { const { privileged } = opts ?? {}; const cmdArr = lodash_1.default.isArray(cmd) ? cmd : [cmd]; const fullCmd = ['shell']; if (privileged) { logger_1.log.info(`'adb shell ${support_1.util.quote(cmdArr)}' requires root access`); if (await this.isRoot()) { logger_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); } /** * Create a new ADB subprocess with the given arguments * * @param args - Array of command arguments (default: empty array) * @returns A SubProcess instance */ function createSubProcess(args = []) { // add the default arguments const finalArgs = [...this.executable.defaultArgs, ...args]; logger_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 * @deprecated Use this.adbPort instead * * @returns The ADB server port number */ function getAdbServerPort() { return this.adbPort; } /** * Retrieve the current emulator port from _adb devices_ output. * * @returns The emulator port number * @throws {Error} If no devices are connected or emulator port cannot be found */ async function getEmulatorPort() { logger_1.log.debug('Getting running emulator port'); if (!lodash_1.default.isNil(this.emulatorPort)) { return this.emulatorPort; } try { const devices = await this.getConnectedDevices(); const port = this.getPortFromEmulatorString(devices[0].udid); if (port) { return port; } else { throw new Error(`Emulator port not found`); } } catch (e) { const error = e; throw new Error(`No devices connected. Original error: ${error.message}`); } } /** * Retrieve the current emulator port by parsing emulator name string. * * @param emStr - The emulator string (e.g., 'emulator-5554') * @returns The port number if found, false otherwise */ function getPortFromEmulatorString(emStr) { const portPattern = /emulator-(\d+)/; const match = portPattern.exec(emStr); return match ? parseInt(match[1], 10) : false; } /** * Retrieve the list of currently connected emulators. * * @param opts - Options for device retrieval * @returns Array of connected emulator devices * @throws {Error} If error occurs while getting emulators */ async function getConnectedEmulators(opts = {}) { logger_1.log.debug('Getting connected emulators'); try { const devices = await this.getConnectedDevices(opts); const emulators = []; for (const device of devices) { const port = this.getPortFromEmulatorString(device.udid); if (port) { device.port = port; emulators.push(device); } } logger_1.log.debug(`${support_1.util.pluralize('emulator', emulators.length, true)} connected`); return emulators; } catch (e) { const error = e; throw new Error(`Error getting emulators. Original error: ${error.message}`); } } /** * Set _emulatorPort_ property of the current class. * * @param emPort - The emulator port number */ function setEmulatorPort(emPort) { this.emulatorPort = emPort; } /** * Set the identifier of the current device (_this.curDeviceId_). * * @param deviceId - The device identifier */ function setDeviceId(deviceId) { logger_1.log.debug(`Setting device id to ${deviceId}`); this.curDeviceId = deviceId; const 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 current device object. * * @param deviceObj - The device object containing udid and other properties */ 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. * * @param avdName - The name of the AVD to find * @returns The device object if found, null otherwise * @throws {Error} If error occurs while getting AVD */ async function getRunningAVD(avdName) { logger_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_1.log.debug(`Found emulator '${avdName}' on port ${emulator.port}`); this.setDeviceId(emulator.udid); return emulator; } } logger_1.log.debug(`Emulator '${avdName}' not running`); return null; } catch (e) { const error = e; throw new Error(`Error getting AVD. Original error: ${error.message}`); } } /** * Get the object for the currently running emulator with retry. * * @param avdName - The name of the AVD to find * @param timeoutMs - Maximum time to wait (default: 20000ms) * @returns The device object if found, null otherwise * @throws {Error} If error occurs while getting AVD with retry */ async function getRunningAVDWithRetry(avdName, timeoutMs = 20000) { try { return (await (0, asyncbox_1.waitForCondition)(async () => { try { return await this.getRunningAVD(avdName.replace('@', '')); } catch (e) { const error = e; logger_1.log.debug(error.message); return false; } }, { waitMs: timeoutMs, intervalMs: 1000, })); } catch (e) { const error = e; throw new Error(`Error getting AVD with retry. Original error: ${error.message}`); } } /** * Shutdown all running emulators by killing their processes. * * @throws {Error} If error occurs while killing emulators */ async function killAllEmulators() { let cmd; let 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) { const error = e; throw new Error(`Error killing emulators. Original error: ${error.message}`); } } /** * Kill emulator with the given name. No error * is thrown if given avd does not exist/is not running. * * @param avdName - The name of the AVD to kill (null to kill current AVD) * @param timeout - Maximum time to wait for emulator to be killed (default: 60000ms) * @returns True if emulator was killed, false if it was not running * @throws {Error} If emulator is still running after timeout */ async function killEmulator(avdName = null, timeout = 60000) { if (support_1.util.hasValue(avdName)) { logger_1.log.debug(`Killing avd '${avdName}'`); const device = await this.getRunningAVD(avdName); if (!device) { logger_1.log.info(`No avd with name '${avdName}' running. Skipping kill step.`); return false; } } else { // killing the current avd logger_1.log.debug(`Killing avd with id '${this.curDeviceId}'`); if (!(await this.isEmulatorConnected())) { logger_1.log.debug(`Emulator with id '${this.curDeviceId}' not connected. Skipping kill step`); return false; } } await this.adbExec(['emu', 'kill']); logger_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_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. * * @param avdName - The name of the AVD to launch * @param opts - Launch options * @returns The SubProcess instance for the launched emulator * @throws {Error} If emulator fails to launch or boot */ async function launchAVD(avdName, opts = {}) { const { args = [], env = {}, language, country, launchTimeout = 60000, readyTimeout = 60000, retryTimes = 1, } = opts; logger_1.log.debug(`Launching Emulator with AVD ${avdName}, launchTimeout ` + `${launchTimeout}ms and readyTimeout ${readyTimeout}ms`); const emulatorBinaryPath = await this.getSdkBinaryPath('emulator'); let processedAvdName = avdName; if (processedAvdName.startsWith('@')) { processedAvdName = processedAvdName.slice(1); } await this.checkAvdExist(processedAvdName); const launchArgs = ['-avd', processedAvdName]; 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(processedAvdName); 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) { const error = e; logger_1.log.info(`The -delay-adb emulator startup detection feature will not be enabled. ` + `Original error: ${error.message}`); } } } else { logger_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 : support_1.util.shellParse(`${args}`))); } logger_1.log.debug(`Running '${emulatorBinaryPath}' with args: ${support_1.util.quote(launchArgs)}`); if (!lodash_1.default.isEmpty(env)) { logger_1.log.debug(`Customized emulator environment: ${JSON.stringify(env)}`); } const proc = new teen_process_1.SubProcess(emulatorBinaryPath, launchArgs, { env: { ...process.env, ...env }, }); await proc.start(0); for (const streamName of ['stderr', 'stdout']) { proc.on(`line-${streamName}`, (line) => logger_1.log.debug(`[AVD OUTPUT] ${line}`)); } proc.on('die', (code, signal) => { logger_1.log.warn(`Emulator avd ${processedAvdName} exited with code ${code}${signal ? `, signal ${signal}` : ''}`); }); await (0, asyncbox_1.retry)(retryTimes, async () => await this.getRunningAVDWithRetry(processedAvdName, 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) { const error = e; throw new Error(`'${processedAvdName}' Emulator has failed to boot: ${error.stderr || error.message}`); } } await this.waitForEmulatorReady(Math.trunc(readyTimeout - timer.getDuration().asMilliSeconds)); return proc; } /** * Get the adb version. The result of this method is cached. * * @returns Version information object * @throws {Error} If error occurs while getting adb version */ exports.getVersion = lodash_1.default.memoize(async function getVersion() { let stdout; try { stdout = await this.adbExec('version'); } catch (e) { const error = e; throw new Error(`Error getting adb version: ${error.stderr || error.message}`); } const result = {}; const binaryVersionMatch = BINARY_VERSION_PATTERN.exec(stdout); if (binaryVersionMatch) { result.binary = { version: semver.coerce(binaryVersionMatch[1])?.version || binaryVersionMatch[1], build: parseInt(binaryVersionMatch[2], 10), }; } const bridgeVersionMatch = BRIDGE_VERSION_PATTERN.exec(stdout); if (bridgeVersionMatch) { result.bridge = { version: semver.coerce(bridgeVersionMatch[1])?.version || bridgeVersionMatch[1], }; } return result; }); /** * Check if the current emulator is ready to accept further commands (booting completed). * * @param timeoutMs - Maximum time to wait (default: 20000ms) * @throws {Error} If emulator is not ready within the timeout period */ async function waitForEmulatorReady(timeoutMs = 20000) { logger_1.log.debug(`Waiting up to ${timeoutMs}ms for the emulator to be ready`); const requiredServicesRe = REQUIRED_SERVICES.map((name) => new RegExp(`\\b${name}:`)); let services; const timer = new support_1.timing.Timer().start(); let isFirstCheck = true; let isBootCompleted = false; try { await (0, asyncbox_1.waitForCondition)(async () => { if (isFirstCheck) { isFirstCheck = false; } else { logger_1.log.debug(`${timer.getDuration().asMilliSeconds.toFixed(0)}ms elapsed since ` + `emulator readiness check has started`); } try { if (!isBootCompleted) { const [bootCompleted, bootAnimState] = await Promise.all([ this.shell(['getprop', 'sys.boot_completed']), this.shell(['getprop', 'init.svc.bootanim']), ]); if (bootCompleted.trim() !== '1' || !['stopped', ''].includes(bootAnimState.trim())) { logger_1.log.debug(`Current status: sys.boot_completed=${bootCompleted.trim()}, ` + `init.svc.bootanim=${bootAnimState.trim()}`); return false; } isBootCompleted = true; } const servicesOutput = await this.shell(['service', 'list']); services = servicesOutput; if (!servicesOutput || !requiredServicesRe.every((pattern) => pattern.test(servicesOutput))) { logger_1.log.debug(`Running services: ${servicesOutput}`); return false; } return true; } catch (err) { const error = err; logger_1.log.debug(`Intermediate error: ${error.message}`); return false; } }, { waitMs: timeoutMs, intervalMs: 3000, }); } catch { let suffix = ''; const servicesValue = services; if (servicesValue) { const missingServices = lodash_1.default.zip(REQUIRED_SERVICES, requiredServicesRe) .filter(([, pattern]) => !pattern.test(servicesValue)) .map(([name]) => name); suffix = ` (${missingServices} service${missingServices.length === 1 ? ' is' : 's are'} not running)`; } throw new Error(`Emulator is not ready within ${timeoutMs}ms${suffix}`); } const elapsedMs = timer.getDuration().asMilliSeconds; // Only log if the wait took a noticeable amount of time if (elapsedMs > 100) { logger_1.log.info(`Emulator is ready after ${elapsedMs}ms`); } } /** * Check if the current device is ready to accept further commands (booting completed). * * @param appDeviceReadyTimeout - Timeout in seconds (default: 30) * @throws {Error} If device is not ready within the timeout period */ async function waitForDevice(appDeviceReadyTimeout = 30) { const timeoutMs = appDeviceReadyTimeout * 1000; let lastErrorMessage = null; try { await (0, asyncbox_1.waitForCondition)(async () => { try { await this.adbExec('wait-for-device', { timeout: Math.trunc(timeoutMs * 0.99) }); await this.ping(); return true; } catch (e) { const error = e; lastErrorMessage = error.message; try { try { await this.reconnect(); } catch { await this.restartAdb(); } await this.getConnectedDevices(); } catch { // Ignore errors during reconnection } return false; } }, { waitMs: timeoutMs, intervalMs: 1000, }); } catch { let suffix = ''; if (lastErrorMessage) { suffix = ` Original error: ${lastErrorMessage}`; } throw new Error(`The device is not ready after ${appDeviceReadyTimeout}s.${suffix}`); } } /** * Reboot the current device and wait until it is completed. * * @param retries - Number of retry attempts (default: 90) * @throws {Error} If reboot fails or device is not ready after reboot */ 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 error = e; const { message } = error; // 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 error; } 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_1.log.debug(msg); throw new Error(msg); }); } /** * Switch adb server root privileges. * * @param isElevated - True to enable root, false to disable * @returns Result object indicating success and whether device was already rooted */ async function changeUserPrivileges(isElevated) { const cmd = isElevated ? 'root' : 'unroot'; const retryIfOffline = async (cmdFunc) => { try { return await cmdFunc(); } catch (err) { const error = 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) => (error.stderr || '').toLowerCase().includes(x))) { logger_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 error; } } }; // 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_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 error = err; const { stderr = '', message } = error; logger_1.log.warn(`Unable to ${cmd} adb daemon. Original error: '${message}'. Stderr: '${stderr}'. Continuing.`); return { isSuccessful: false, wasAlreadyRooted }; } } /** * Switch adb server to root mode * * @returns Result object indicating success and whether device was already rooted */ async function root() { return await this.changeUserPrivileges(true); } /** * Switch adb server to non-root mode. * * @returns Result object indicating success and whether device was already rooted */ async function unroot() { return await this.changeUserPrivileges(false); } /** * Checks whether the current user is root * * @returns True if current user is root, false otherwise */ 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 * * @param cert - Certificate as Buffer or base64-encoded string * @throws {Error} If certificate installation fails */ async function installMitmCertificate(cert) { const openSsl = await getOpenSslForOs(); const tmpRoot = await support_1.tempDir.openDir(); try { const srcCert = node_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_1.log.debug(`Got certificate hash: ${certHash}`); logger_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 = node_path_1.default.resolve(tmpRoot, `${certHash}.0`); await support_1.fs.writeFile(dstCert, dstCertContent); logger_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_1.log.debug(`Uploading the generated certificate from '${dstCert}' to '${CERTS_ROOT}'`); await this.push(dstCert, CERTS_ROOT); logger_1.log.debug('Remounting /system to confirm changes'); await this.adbExec(['remount']); } catch (err) { const error = 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: ${error.message}`); } finally { await support_1.fs.rimraf(tmpRoot); } } /** * Verifies if the given root certificate is already installed on the device. * * @param cert - Certificate as Buffer or base64-encoded string * @returns True if certificate is installed, false otherwise * @throws {Error} If certificate hash cannot be retrieved */ async function isMitmCertificateInstalled(cert) { const openSsl = await getOpenSslForOs(); const tmpRoot = await support_1.tempDir.openDir(); let certHash; try { const tmpCert = node_path_1.default.resolve(tmpRoot, 'source.cer'); await support_1.fs.writeFile(tmpCert, Buffer.isBuffer(cert) ? cert : Buffer.from(cert, 'base64')); const { stdout } = await (0, teen_process_1.exec)(openSsl, ['x509', '-noout', '-hash', '-in', tmpCert]); certHash = stdout.trim(); } catch (err) { const error = err; throw new Error(`Cannot retrieve the certificate hash. ` + `Is the certificate properly encoded into base64-string? ` + `Original error: ${error.message}`); } finally { await support_1.fs.rimraf(tmpRoot); } const dstPath = node_path_1.default.posix.resolve(CERTS_ROOT, `${certHash}.0`); logger_1.log.debug(`Checking if the certificate is already installed at '${dstPath}'`); return await this.fileExists(dstPath); } /** * Creates chunks for the given arguments and executes them in `adb shell`. * This is faster than calling `adb shell` separately for each arg, however * there is a limit for a maximum length of a single adb command. that is why * we need all this complicated logic. * * @param argTransformer - Function to transform each argument into command array * @param args - Array of arguments to process * @throws {Error} If argument transformer returns invalid result or command execution fails */ async function shellChunks(argTransformer, args) { const commands = []; let cmdChunk = []; for (const arg of args) { const nextCmd = argTransformer(arg); if (!lodash_1.default.isArray(nextCmd)) { throw new Error('Argument transformer must result in an array'); } if (lodash_1.default.last(nextCmd) !== ';') { nextCmd.push(';'); } if (nextCmd.join(' ').length + cmdChunk.join(' ').length >= MAX_SHELL_BUFFER_LENGTH) { commands.push(cmdChunk); cmdChunk = []; } cmdChunk = [...cmdChunk, ...nextCmd]; } if (!lodash_1.default.isEmpty(cmdChunk)) { commands.push(cmdChunk); } logger_1.log.debug(`Got the following command chunks to execute: ${JSON.stringify(commands)}`); let lastError = null; for (const cmd of commands) { try { await this.shell(cmd); } catch (e) { lastError = e; } } if (lastError) { throw lastError; } } /** * Transforms the given language and country abbreviations * to AVD arguments array * * @param language