appium-adb
Version:
Android Debug Bridge interface
1,302 lines • 52.6 kB
JavaScript
"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