Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Replace avdmanager usage with faster config reading primitives #549

Merged
merged 4 commits into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,43 @@ function toAvdLocaleArgs (language, country) {
return result;
}

/**
* Retrieves the full path to the Android preferences root
*
* @returns {?string} The full path to the folder or `null` if the folder cannot be found
*/
async function getAndroidPrefsRoot () {
// https://gerrit.pixelexperience.org/plugins/gitiles/development/+/5c11852110eeb03dc5a69111354b383f98d15336/tools/androidprefs/src/com/android/prefs/AndroidLocation.java
let location = null;
if (system.isWindows()) {
let localAppData = process.env.LOCALAPPDATA;
if (!localAppData) {
localAppData = process.env.USERPROFILE;
if (localAppData) {
localAppData = path.resolve(localAppData, 'Local Settings', 'Application Data');
}
}
if (localAppData) {
location = path.resolve(localAppData, 'Android');
}
} else {
const home = process.env.HOME;
if (home) {
location = path.resolve(home, '.android');
}
}

if (location && await fs.exists(location)) {
if (!(await fs.stat(location)).isDirectory()) {
log.debug(`Android config root '${location}' is not a directory`);
return null;
}
return location;
}

return null;
}

export {
getAndroidPlatformAndPath, unzipFile,
getIMEListFromOutput, getJavaForOs, isShowingLockscreen, isCurrentFocusOnKeyguard,
Expand All @@ -851,5 +888,5 @@ export {
APK_INSTALL_TIMEOUT, APKS_INSTALL_TIMEOUT, buildInstallArgs, APK_EXTENSION,
DEFAULT_ADB_EXEC_TIMEOUT, parseManifest, parseAaptStrings, parseAapt2Strings,
formatConfigMarker, parseJsonData, unsignApk, toAvdLocaleArgs, requireSdkRoot,
getSdkRootFromEnv,
getSdkRootFromEnv, getAndroidPrefsRoot,
};
92 changes: 74 additions & 18 deletions lib/tools/adb-emu-commands.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import log from '../logger.js';
import { getAndroidPrefsRoot } from '../helpers';
import _ from 'lodash';
import net from 'net';
import { util, fs } from 'appium-support';
import B from 'bluebird';
import path from 'path';
import ini from 'ini';
import { exec } from 'teen_process';

const PHONE_NUMBER_PATTERN = /^[+]?[(]?[0-9]*[)]?[-\s.]?[0-9]*[-\s.]?[0-9]{2,}$/im;

Expand Down Expand Up @@ -52,6 +52,46 @@ emuMethods.SENSORS = {
HUMIDITY: 'humidity'
};

/**
* @typedef {Object} EmuInfo
* @property {string} name Emulator name, for example `Pixel_XL_API_30`
* @property {string} config Full path to the emulator config .ini file,
* for example `/Users/user/.android/avd/Pixel_XL_API_30.ini`
*/

/**
* Retrieves the list of available Android emulators
*
* @returns {Array<EmuInfo>}
*/
async function listEmulators () {
// https://android.googlesource.com/platform/sdk/+/33f330d963c1bf24667eddb891531abcce9fb4de/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java
const prefsRoot = await getAndroidPrefsRoot();
if (!prefsRoot) {
return [];
}

const avdsRoot = path.resolve(prefsRoot, 'avd');
if (!await fs.exists(avdsRoot)) {
return [];
}

const stats = await fs.stat(avdsRoot);
if (!stats.isDirectory()) {
log.debug(`Virtual devices config root '${avdsRoot}' is not a directory`);
return [];
}

const configs = await fs.glob('*.ini', {
cwd: avdsRoot,
absolute: true,
});
return configs.map((confPath) => {
const avdName = path.basename(confPath).split('.').slice(0, -1).join('.');
return {name: avdName, config: confPath};
}).filter(({name}) => _.trim(name));
}

/**
* Check the emulator state.
*
Expand Down Expand Up @@ -370,7 +410,7 @@ emuMethods.getEmuVersionInfo = async function getEmuVersionInfo () {
/**
* Retrieves emulator image properties from the local file system
*
* @param {string} name Emulator name. Should NOT start with '@' character
* @param {string} avdName Emulator name. Should NOT start with '@' character
* @throws {Error} if there was a failure while extracting the properties
* @returns {Object} The content of emulator image properties file.
* Usually this configuration .ini file has the following content:
Expand All @@ -379,24 +419,40 @@ emuMethods.getEmuVersionInfo = async function getEmuVersionInfo () {
* path.rel=avd/Pixel_XL_API_30.avd
* target=android-30
*/
emuMethods.getEmuImageProperties = async function getEmuImageProperties (name) {
const avdManager = await this.getSdkBinaryPath('avdmanager');
const {stdout} = await exec(avdManager, ['list', 'avd']);
const entryMatch = new RegExp(`^\\s*Name:\\s+${_.escapeRegExp(name)}$(.+)$`, 'sm').exec(stdout);
if (!entryMatch) {
log.debug(stdout);
throw new Error(`Cannot find '${name}' emulator entry in avdmanager output`);
}
const pathMatch = /^\s*Path:\s+(.+)$/m.exec(entryMatch[1]);
if (!pathMatch) {
log.debug(entryMatch[1]);
throw new Error(`Cannot parse '${name}' emulator image location from avdmanager output`);
emuMethods.getEmuImageProperties = async function getEmuImageProperties (avdName) {
const avds = await listEmulators();
const avd = avds.find(({name}) => name === avdName);
if (!avd) {
let msg = `Cannot find '${avdName}' emulator. `;
if (_.isEmpty(avds)) {
msg += `No emulators have been detected on your system`;
} else {
msg += `Available avd names are: ${avds.map(({name}) => name)}`;
}
throw new Error(msg);
}
const configLocation = path.resolve(path.dirname(pathMatch[1]), `${name}.ini`);
if (!await fs.exists(configLocation)) {
throw new Error(`Cannot find '${name}' emulator config at '${configLocation}`);
return ini.parse(await fs.readFile(avd.config, 'utf8'));
};

/**
* Check if given emulator exists in the list of available avds.
*
* @param {string} avdName - The name of emulator to verify for existence.
* Should NOT start with '@' character
* @throws {Error} If the emulator with given name does not exist.
*/
emuMethods.checkAvdExist = async function checkAvdExist (avdName) {
const avds = await listEmulators();
if (!avds.some(({name}) => name === avdName)) {
let msg = `Avd '${avdName}' is not available. `;
if (_.isEmpty(avds)) {
msg += `No emulators have been detected on your system`;
} else {
msg += `Please select your avd name from one of these: '${avds.map(({name}) => name)}'`;
}
throw new Error(msg);
}
return ini.parse(await fs.readFile(configLocation, 'utf8'));
return true;
};

export default emuMethods;
10 changes: 6 additions & 4 deletions lib/tools/apk-signing.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,15 +265,17 @@ apkSigningMethods.checkApkCert = async function checkApkCert (appPath, pkg, opts
const output = await this.executeApksigner(['verify', '--print-certs', appPath]);
const hasMatch = hashMatches(output);
if (hasMatch) {
log.debug(`'${appPath}' is signed with the default certificate`);
log.info(`'${appPath}' is signed with the ` +
`${this.useKeystore ? 'keystore' : 'default'} certificate`);
} else {
log.debug(`'${appPath}' is signed with a non-default certificate`);
log.info(`'${appPath}' is signed with a ` +
`non-${this.useKeystore ? 'keystore' : 'default'} certificate`);
}
return !requireDefaultCert || hasMatch;
return (!this.useKeystore && !requireDefaultCert) || hasMatch;
} catch (err) {
// check if there is no signature
if (_.includes(err.stderr, APKSIGNER_VERIFY_FAIL)) {
log.debug(`'${appPath}' is not signed`);
log.info(`'${appPath}' is not signed`);
return false;
}
throw new Error(`Cannot verify the signature of '${appPath}'. ` +
Expand Down
39 changes: 1 addition & 38 deletions lib/tools/system-calls.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import log from '../logger.js';
import B from 'bluebird';
import { system, fs, util, tempDir, timing } from 'appium-support';
import {
getSdkToolsVersion, getBuildToolsDirs, toAvdLocaleArgs,
getBuildToolsDirs, toAvdLocaleArgs,
getOpenSslForOs, DEFAULT_ADB_EXEC_TIMEOUT, getSdkRootFromEnv
} from '../helpers';
import { exec, SubProcess } from 'teen_process';
Expand Down Expand Up @@ -891,43 +891,6 @@ systemCallMethods.getVersion = _.memoize(async function getVersion () {
return result;
});

/**
* Check if given emulator exists in the list of available avds.
*
* @param {string} avdName - The name of emulator to verify for existence.
* @throws {Error} If the emulator with given name does not exist.
*/
systemCallMethods.checkAvdExist = async function checkAvdExist (avdName) {
let cmd, result;
try {
cmd = await this.getSdkBinaryPath('emulator');
result = await exec(cmd, ['-list-avds']);
} catch (e) {
let unknownOptionError = new RegExp('unknown option: -list-avds', 'i').test(e.stderr);
if (!unknownOptionError) {
throw new Error(`Error executing checkAvdExist. Original error: '${e.message}'; ` +
`Stderr: '${(e.stderr || '').trim()}'; Code: '${e.code}'`);

}
const sdkVersion = await getSdkToolsVersion();
let binaryName = 'android';
if (sdkVersion) {
if (sdkVersion.major >= 25) {
binaryName = 'avdmanager';
}
} else {
log.warn(`Defaulting binary name to '${binaryName}', because SDK version cannot be parsed`);
}
// If -list-avds option is not available, use android command as an alternative
cmd = await this.getSdkBinaryPath(binaryName);
result = await exec(cmd, ['list', 'avd', '-c']);
}
if (result.stdout.indexOf(avdName) === -1) {
let existings = `(${result.stdout.trim().replace(/[\n]/g, '), (')})`;
throw new Error(`Avd '${avdName}' is not available. please select your avd name from one of these: '${existings}'`);
}
};

/**
* Check if the current emulator is ready to accept further commands (booting completed).
*
Expand Down
2 changes: 1 addition & 1 deletion test/functional/adb-emu-commands-e2e-specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('adb emu commands', function () {
});

describe('getEmuImageProperties', function () {
it.skip('should get emulator image properties', async function () {
it('should get emulator image properties', async function () {
const name = await adb.execEmuConsoleCommand(['avd', 'name']);
const {target} = await adb.getEmuImageProperties(name);
const apiMatch = /\d+/.exec(target);
Expand Down