appium-adb-test
Version:
Android Debug Bridge interface
488 lines (452 loc) • 16.8 kB
JavaScript
import path from 'path';
import log from '../logger.js';
import B from 'bluebird';
import { system, fs } from 'appium-support';
import { getDirectories } from '../helpers';
import { exec, SubProcess } from 'teen_process';
import { sleep, retry, retryInterval } from 'asyncbox';
let systemCallMethods = {};
const DEFAULT_ADB_EXEC_TIMEOUT = 20000; // in milliseconds
systemCallMethods.getSdkBinaryPath = async function (binaryName) {
log.info(`Checking whether ${binaryName} is present`);
if (this.sdkRoot) {
return this.getBinaryFromSdkRoot(binaryName);
} else {
log.warn(`The ANDROID_HOME environment variable is not set to the Android SDK ` +
`root directory path. ANDROID_HOME is required for compatibility ` +
`with SDK 23+. Checking along PATH for ${binaryName}.`);
return await this.getBinaryFromPath(binaryName);
}
};
systemCallMethods.getCommandForOS = function () {
let cmd = "which";
if (system.isWindows()) {
cmd = "where";
}
return cmd;
};
systemCallMethods.getBinaryNameForOS = function (binaryName) {
if (system.isWindows()) {
if (binaryName === "android") {
binaryName += ".bat";
} else {
if (binaryName.indexOf(".exe", binaryName.length - 4) === -1) {
binaryName += ".exe";
}
}
}
return binaryName;
};
systemCallMethods.getBinaryFromSdkRoot = async function (binaryName) {
let binaryLoc = null;
binaryName = this.getBinaryNameForOS(binaryName);
let binaryLocs = [path.resolve(this.sdkRoot, "platform-tools", binaryName),
path.resolve(this.sdkRoot, "tools", binaryName)];
// get subpaths for currently installed build tool directories
let buildToolDirs = [];
buildToolDirs = await getDirectories(path.resolve(this.sdkRoot, "build-tools"));
for (let versionDir of buildToolDirs) {
binaryLocs.push(path.resolve(this.sdkRoot, "build-tools", versionDir, binaryName));
}
for (let loc of binaryLocs) {
let flag = await fs.exists(loc);
if (flag) {
binaryLoc = loc;
}
}
if (binaryLoc === null) {
throw new Error(`Could not find ${binaryName} in tools, platform-tools, ` +
`or supported build-tools under ${this.sdkRoot} ` +
`do you have the Android SDK installed at this location?`);
}
binaryLoc = binaryLoc.trim();
log.info(`Using ${binaryName} from ${binaryLoc}`);
return binaryLoc;
};
systemCallMethods.getBinaryFromPath = async function (binaryName) {
let binaryLoc = null;
let cmd = this.getCommandForOS();
try {
let {stdout} = await exec(cmd, [binaryName]);
log.info(`Using ${binaryName} from ${stdout}`);
// TODO write a test for binaries with spaces.
binaryLoc = stdout.trim();
return binaryLoc;
} catch (e) {
log.errorAndThrow(`Could not find ${binaryName} Please set the ANDROID_HOME ` +
`environment variable with the Android SDK root directory path.`);
}
};
systemCallMethods.getConnectedDevices = async function () {
log.debug("Getting connected devices...");
try {
let {stdout} = await exec(this.executable.path, ['devices']);
// expecting adb devices to return output as
// List of devices attached
// emulator-5554 device
let startingIndex = stdout.indexOf("List of devices");
if (startingIndex === -1) {
throw new Error(`Unexpected output while trying to get devices. output was: ${stdout}`);
} else {
// slicing ouput we care about.
stdout = stdout.slice(startingIndex);
let devices = [];
for (let line of stdout.split("\n")) {
if (line.trim() !== "" &&
line.indexOf("List of devices") === -1 &&
line.indexOf("* daemon") === -1 &&
line.indexOf("offline") === -1) {
let lineInfo = line.split("\t");
// state is either "device" or "offline", afaict
devices.push({udid: lineInfo[0], state: lineInfo[1]});
}
}
log.debug(`${devices.length} device(s) connected`);
return devices;
}
} catch (e) {
log.errorAndThrow(`Error while getting connected devices. Original error: ${e.message}`);
}
};
systemCallMethods.getDevicesWithRetry = async function (timeoutMs = 20000) {
let start = Date.now();
log.debug("Trying to find a connected android device");
let getDevices = async () => {
if ((Date.now() - start) > timeoutMs) {
throw new Error("Could not find a connected Android device.");
}
try {
let devices = await this.getConnectedDevices();
if (devices.length < 1) {
log.debug("Could not find devices, restarting adb server...");
await this.restartAdb();
// cool down
await sleep(200);
return await getDevices();
}
return devices;
} catch (e) {
log.debug("Could not find devices, restarting adb server...");
await this.restartAdb();
// cool down
await sleep(200);
return await getDevices();
}
};
return await getDevices();
};
systemCallMethods.restartAdb = async function () {
if (!this.suppressKillServer) {
log.debug('Restarting adb');
try {
await exec(this.executable.path, ['kill-server']);
} catch (e) {
log.error("Error killing ADB server, going to see if it's online anyway");
}
}
};
systemCallMethods.adbExec = async function (cmd, opts = {}) {
if (!cmd) {
throw new Error("You need to pass in a command to adbExec()");
}
// setting default timeout for each command to prevent infinite wait.
opts.timeout = opts.timeout || DEFAULT_ADB_EXEC_TIMEOUT;
let execFunc = async () => {
let linkerWarningRe = /^WARNING: linker.+$/m;
try {
if (!(cmd instanceof Array)) {
cmd = [cmd];
}
let args = this.executable.defaultArgs.concat(cmd);
log.debug(`Running '${this.executable.path}' with args: ` +
`${JSON.stringify(args)}`);
let {stdout} = await exec(this.executable.path, args, opts);
// sometimes ADB prints out stupid stdout warnings that we don't want
// to include in any of the response data, so let's strip it out
stdout = stdout.replace(linkerWarningRe, '').trim();
return stdout;
} catch (e) {
let protocolFaultError = new RegExp("protocol fault \\(no status\\)", "i").test(e);
let deviceNotFoundError = new RegExp("error: device ('.+' )?not found", "i").test(e);
if (protocolFaultError || deviceNotFoundError) {
log.info(`Error sending command, reconnecting device and retrying: ${cmd}`);
await sleep(1000);
await this.getDevicesWithRetry();
}
if (e.stdout) {
let stdout = e.stdout;
stdout = stdout.replace(linkerWarningRe, '').trim();
return stdout;
}
throw new Error(`Error executing adbExec. Original error: '${e.message}'; ` +
`Stderr: '${(e.stderr || '').trim()}'; Code: '${e.code}'`);
}
};
return await retry(2, execFunc);
};
systemCallMethods.shell = async function (cmd, opts = {}) {
if (!await this.isDeviceConnected()) {
throw new Error(`No device connected, cannot run adb shell command '${cmd.join(' ')}'`);
}
let execCmd = ['shell'];
if (cmd instanceof Array) {
execCmd = execCmd.concat(cmd);
} else {
execCmd.push(cmd);
}
return await this.adbExec(execCmd, opts);
};
systemCallMethods.createSubProcess = function (args = []) {
// add the default arguments
args = this.executable.defaultArgs.concat(args);
log.debug(`Creating ADB subprocess with args: ${JSON.stringify(args)}`);
return new SubProcess(this.getAdbPath(), args);
};
// TODO can probably deprecate this now that the logic is just to read
// this.adbPort
systemCallMethods.getAdbServerPort = function () {
return this.adbPort;
};
systemCallMethods.getEmulatorPort = async function () {
log.debug("Getting running emulator port");
if (this.emulatorPort !== null) {
return 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) {
log.errorAndThrow(`No devices connected. Original error: ${e.message}`);
}
};
systemCallMethods.getPortFromEmulatorString = function (emStr) {
let portPattern = /emulator-(\d+)/;
if (portPattern.test(emStr)) {
return parseInt(portPattern.exec(emStr)[1], 10);
}
return false;
};
systemCallMethods.getConnectedEmulators = async function () {
try {
log.debug("Getting connected emulators");
let devices = await this.getConnectedDevices();
let emulators = [];
for (let device of devices) {
let port = this.getPortFromEmulatorString(device.udid);
if (port) {
device.port = port;
emulators.push(device);
}
}
log.debug(`${emulators.length} emulator(s) connected`);
return emulators;
} catch (e) {
log.errorAndThrow(`Error getting emulators. Original error: ${e.message}`);
}
};
systemCallMethods.setEmulatorPort = function (emPort) {
this.emulatorPort = emPort;
};
systemCallMethods.setDeviceId = function (deviceId) {
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);
};
systemCallMethods.setDevice = function (deviceObj) {
let deviceId = deviceObj.udid;
let emPort = this.getPortFromEmulatorString(deviceId);
this.setEmulatorPort(emPort);
this.setDeviceId(deviceId);
};
systemCallMethods.getRunningAVD = async function (avdName) {
try {
log.debug(`Trying to find ${avdName} emulator`);
let emulators = await this.getConnectedEmulators();
for (let emulator of emulators) {
this.setEmulatorPort(emulator.port);
let runningAVDName = await this.sendTelnetCommand("avd name");
if (avdName === runningAVDName) {
log.debug(`Found emulator ${avdName} in port ${emulator.port}`);
this.setDeviceId(emulator.udid);
return emulator;
}
}
log.debug(`Emulator ${avdName} not running`);
return null;
} catch (e) {
log.errorAndThrow(`Error getting AVD. Original error: ${e.message}`);
}
};
systemCallMethods.getRunningAVDWithRetry = async function (avdName, timeoutMs = 20000) {
try {
let start = Date.now();
while ((Date.now() - start) < timeoutMs) {
try {
let runningAVD = await this.getRunningAVD(avdName.replace('@', ''));
if (runningAVD) {
return runningAVD;
}
} catch (e) {
// Do nothing.
log.info(`Couldn't get running AVD, will retry. Error was: ${e.message}`);
}
// cool down
await sleep(200);
}
log.errorAndThrow(`Could not find ${avdName} emulator.`);
} catch (e) {
log.errorAndThrow(`Error getting AVD with retry. Original error: ${e.message}`);
}
};
systemCallMethods.killAllEmulators = async function () {
let cmd, args;
if (system.isWindows()) {
cmd = 'TASKKILL';
args = ['TASKKILL', '/IM', 'emulator.exe'];
} else {
cmd = '/usr/bin/killall';
args = ['-m', 'emulator*'];
}
try {
await exec(cmd, args);
} catch (e) {
log.errorAndThrow(`Error killing emulators. Original error: ${e.message}`);
}
};
systemCallMethods.killEmulator = async function (avdName) {
log.debug(`killing avd '${avdName}'`);
let device = await this.getRunningAVD(avdName);
if (device) {
await this.adbExec(['emu', 'kill']);
log.info(`successfully killed emulator '${avdName}'`);
} else {
log.info(`no avd with name '${avdName}' running. skipping kill step.`);
}
};
systemCallMethods.launchAVD = async function (avdName, avdArgs, language, country,
avdLaunchTimeout = 60000, avdReadyTimeout = 60000, retryTimes = 1) {
log.debug(`Launching Emulator with AVD ${avdName}, launchTimeout` +
`${avdLaunchTimeout} ms and readyTimeout ${avdReadyTimeout} ms`);
let emulatorBinaryPath = await this.getSdkBinaryPath("emulator");
if (avdName[0] === "@") {
avdName = avdName.substr(1);
}
await this.checkAvdExist(avdName);
let launchArgs = ["-avd", avdName];
if (typeof language === "string") {
log.debug(`Setting Android Device Language to ${language}`);
launchArgs.push("-prop", `persist.sys.language=${language.toLowerCase()}`);
}
if (typeof country === "string") {
log.debug(`Setting Android Device Country to ${country}`);
launchArgs.push("-prop", `persist.sys.country=${country.toUpperCase()}`);
}
let locale;
if (typeof language === "string" && typeof country === "string") {
locale = language.toLowerCase() + "-" + country.toUpperCase();
} else if (typeof language === "string") {
locale = language.toLowerCase();
} else if (typeof country === "string") {
locale = country;
}
if (typeof locale === "string") {
log.debug(`Setting Android Device Locale to ${locale}`);
launchArgs.push("-prop", `persist.sys.locale=${locale}`);
}
if (typeof avdArgs === "string") {
avdArgs = avdArgs.split(" ");
launchArgs = launchArgs.concat(avdArgs);
}
let proc = new SubProcess(emulatorBinaryPath, launchArgs);
await proc.start(0);
proc.on('output', (stdout, stderr) => {
log.info(`[AVD OUTPUT] ${stdout || stderr}`);
});
await retry(retryTimes, this.getRunningAVDWithRetry.bind(this), avdName, avdLaunchTimeout);
await this.waitForEmulatorReady(avdReadyTimeout);
return proc;
};
systemCallMethods.checkAvdExist = async function (avdName) {
let cmd = await this.getSdkBinaryPath('android');
let args = ['list', 'avd', '-c'];
let {stdout} = await exec(cmd, args);
if (stdout.indexOf(avdName) === -1) {
let existings = `(${stdout.trim().replace(/[\n]/g, '), (')})`;
log.errorAndThrow(`Avd '${avdName}' is not available. please select your avd name from one of these: '${existings}'`);
}
};
systemCallMethods.waitForEmulatorReady = async function (timeoutMs = 20000) {
let start = Date.now();
log.debug("Waiting until emulator is ready");
while ((Date.now() - start) < timeoutMs) {
try {
let stdout = await this.shell(["getprop", "init.svc.bootanim"]);
if (stdout.indexOf('stopped') > -1) {
return;
}
} catch (e) {
// do nothing
}
await sleep(3000);
}
log.errorAndThrow('Emulator not ready');
};
systemCallMethods.waitForDevice = async function (appDeviceReadyTimeout = 30) {
this.appDeviceReadyTimeout = appDeviceReadyTimeout;
const retries = 3;
const timeout = parseInt(this.appDeviceReadyTimeout, 10) / retries * 1000;
await retry(retries, async () => {
try {
await this.adbExec('wait-for-device', {timeout});
await this.ping();
} catch (e) {
await this.restartAdb();
await this.getConnectedDevices();
log.errorAndThrow(`Error in waiting for device. Original error: '${e.message}'. ` +
`Retrying by restarting ADB`);
}
});
};
systemCallMethods.reboot = async function () {
await this.shell(['stop']);
await B.delay(2000); // let the emu finish stopping;
await this.setDeviceProperty('sys.boot_completed', 0);
await this.shell(['start']);
await retryInterval(90, 1000, async () => {
let booted = await this.getDeviceProperty('sys.boot_completed');
if (booted === '1') {
return;
} else {
log.errorAndThrow('Waiting for reboot this takes time');
}
});
};
systemCallMethods.fileExists = async function (remotePath) {
let files = await this.ls(remotePath);
return files.length > 0;
};
systemCallMethods.ls = async function (remotePath) {
try {
let stdout = await this.shell(['ls', remotePath]);
let lines = stdout.split("\n");
return lines.map((l) => l.trim())
.filter(Boolean)
.filter((l) => l.indexOf("No such file") === -1);
} catch (err) {
if (err.message.indexOf('No such file or directory') === -1) {
throw err;
}
return [];
}
};
export default systemCallMethods;