appium-adb
Version:
Android Debug Bridge interface
252 lines • 9.88 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.execBundletool = execBundletool;
exports.getDeviceSpec = getDeviceSpec;
exports.installMultipleApks = installMultipleApks;
exports.installApks = installApks;
exports.extractBaseApk = extractBaseApk;
exports.extractLanguageApk = extractLanguageApk;
exports.isTestPackageOnlyError = isTestPackageOnlyError;
const teen_process_1 = require("teen_process");
const logger_1 = require("../logger");
const node_path_1 = __importDefault(require("node:path"));
const lodash_1 = __importDefault(require("lodash"));
const support_1 = require("@appium/support");
const lru_cache_1 = require("lru-cache");
const helpers_1 = require("../helpers");
const async_lock_1 = __importDefault(require("async-lock"));
const bluebird_1 = __importDefault(require("bluebird"));
const BASE_APK = 'base-master.apk';
const LANGUAGE_APK = (lang) => `base-${lang}.apk`;
const APKS_CACHE = new lru_cache_1.LRUCache({
max: 10,
dispose: (extractedFilesRoot) => support_1.fs.rimraf(extractedFilesRoot),
});
const APKS_CACHE_GUARD = new async_lock_1.default();
const BUNDLETOOL_TIMEOUT_MS = 4 * 60 * 1000;
const APKS_INSTALL_TIMEOUT = helpers_1.APK_INSTALL_TIMEOUT * 2;
process.on('exit', () => {
if (!APKS_CACHE.size) {
return;
}
const paths = [...APKS_CACHE.values()];
logger_1.log.debug(`Performing cleanup of ${paths.length} cached .apks ` + support_1.util.pluralize('package', paths.length));
for (const appPath of paths) {
try {
// Asynchronous calls are not supported in onExit handler
support_1.fs.rimrafSync(appPath);
}
catch (e) {
logger_1.log.warn(e.message);
}
}
});
/**
* Extracts the particular apks package into a temporary folder,
* finds and returns the full path to the file contained in this apk.
* The resulting temporary path, where the .apks file has been extracted,
* will be stored into the internal LRU cache for better performance.
*
* @param apks - The full path to the .apks file
* @param dstPath - The relative path to the destination file,
* which is going to be extracted, where each path component is an array item
* @returns Full path to the extracted file
* @throws {Error} If the requested item does not exist in the extracted archive or the provides
* apks file is not a valid bundle
*/
async function extractFromApks(apks, dstPath) {
const normalizedDstPath = lodash_1.default.isArray(dstPath) ? dstPath : [dstPath];
return await APKS_CACHE_GUARD.acquire(apks, async () => {
// It might be that the original file has been replaced,
// so we need to keep the hash sums instead of the actual file paths
// as caching keys
const apksHash = await support_1.fs.hash(apks);
logger_1.log.debug(`Calculated '${apks}' hash: ${apksHash}`);
if (APKS_CACHE.has(apksHash)) {
const cachedRoot = APKS_CACHE.get(apksHash);
if (cachedRoot) {
const resultPath = node_path_1.default.resolve(cachedRoot, ...normalizedDstPath);
if (await support_1.fs.exists(resultPath)) {
return resultPath;
}
}
APKS_CACHE.delete(apksHash);
}
const tmpRoot = await support_1.tempDir.openDir();
logger_1.log.debug(`Unpacking application bundle at '${apks}' to '${tmpRoot}'`);
await (0, helpers_1.unzipFile)(apks, tmpRoot);
const resultPath = node_path_1.default.resolve(tmpRoot, ...normalizedDstPath);
if (!(await support_1.fs.exists(resultPath))) {
throw new Error(`${normalizedDstPath.join(node_path_1.default.sep)} cannot be found in '${apks}' bundle. ` +
`Does the archive contain a valid application bundle?`);
}
APKS_CACHE.set(apksHash, tmpRoot);
return resultPath;
});
}
/**
* Executes bundletool utility with given arguments and returns the actual stdout
*
* @param args - the list of bundletool arguments
* @param errorMsg - The customized error message string
* @returns the actual command stdout
* @throws {Error} If bundletool jar does not exist in PATH or there was an error while
* executing it
*/
async function execBundletool(args, errorMsg) {
await this.initBundletool();
const binaries = this.binaries;
args = ['-jar', binaries.bundletool, ...args];
const env = process.env;
if (this.adbPort) {
env.ANDROID_ADB_SERVER_PORT = `${this.adbPort}`;
}
if (this.adbHost) {
env.ANDROID_ADB_SERVER_HOST = this.adbHost;
}
logger_1.log.debug(`Executing bundletool with arguments: ${JSON.stringify(args)}`);
let stdout;
try {
({ stdout } = await (0, teen_process_1.exec)(await (0, helpers_1.getJavaForOs)(), args, {
env,
timeout: BUNDLETOOL_TIMEOUT_MS,
}));
logger_1.log.debug(`Command stdout: ${lodash_1.default.truncate(stdout, { length: 300 })}`);
return stdout;
}
catch (e) {
const err = e;
if (err.stdout) {
logger_1.log.debug(`Command stdout: ${err.stdout}`);
}
if (err.stderr) {
logger_1.log.debug(`Command stderr: ${err.stderr}`);
}
throw new Error(`${errorMsg}. Original error: ${err.message}`);
}
}
/**
*
* @param specLocation - The full path to the generated device spec location
* @returns The same `specLocation` value
* @throws {Error} If it is not possible to retrieve the spec for the current device
*/
async function getDeviceSpec(specLocation) {
const args = [
'get-device-spec',
'--adb',
this.executable.path,
'--device-id',
this.curDeviceId,
'--output',
specLocation,
];
logger_1.log.debug(`Getting the spec for the device '${this.curDeviceId}'`);
await this.execBundletool(args, 'Cannot retrieve the device spec');
return specLocation;
}
/**
* Installs the given apks into the device under test
*
* @param apkPathsToInstall - The full paths to install apks
* @param options - Installation options
*/
async function installMultipleApks(apkPathsToInstall, options = {}) {
const installArgs = (0, helpers_1.buildInstallArgs)(await this.getApiLevel(), options);
return await this.adbExec(['install-multiple', ...installArgs, ...apkPathsToInstall], {
// @ts-ignore This validation works
timeout: isNaN(Number(options.timeout)) ? undefined : options.timeout,
timeoutCapName: options.timeoutCapName,
});
}
/**
* Installs the given .apks package into the device under test
*
* @param apks - The full path to the .apks file
* @param options - Installation options
* @throws {Error} If the .apks bundle cannot be installed
*/
async function installApks(apks, options = {}) {
const { grantPermissions, allowTestPackages, timeout } = options;
const args = [
'install-apks',
'--adb',
this.executable.path,
'--apks',
apks,
'--timeout-millis',
`${timeout || APKS_INSTALL_TIMEOUT}`,
'--device-id',
this.curDeviceId,
];
if (allowTestPackages) {
args.push('--allow-test-only');
}
const tasks = [
this.execBundletool(args, `Cannot install '${node_path_1.default.basename(apks)}' to the device ${this.curDeviceId}`),
];
if (grantPermissions) {
tasks.push(this.getApkInfo(apks));
}
const [, apkInfo] = await bluebird_1.default.all(tasks);
if (grantPermissions && apkInfo) {
// TODO: Simplify it after https://github.com/google/bundletool/issues/246 is implemented
await this.grantAllPermissions(apkInfo.name);
}
}
/**
* Extracts and returns the full path to the master .apk file inside the bundle.
*
* @param apks - The full path to the .apks file
* @returns The full path to the master bundle .apk
* @throws {Error} If there was an error while extracting/finding the file
*/
async function extractBaseApk(apks) {
return await extractFromApks(apks, ['splits', BASE_APK]);
}
/**
* Extracts and returns the full path to the .apk, which contains the corresponding
* resources for the given language in the .apks bundle.
*
* @param apks - The full path to the .apks file
* @param language - The language abbreviation. The default language is
* going to be selected if it is not set.
* @returns The full path to the corresponding language .apk or the master .apk
* if language split is not enabled for the bundle.
* @throws {Error} If there was an error while extracting/finding the file
*/
async function extractLanguageApk(apks, language = null) {
if (language) {
try {
return await extractFromApks(apks, ['splits', LANGUAGE_APK(language)]);
}
catch (e) {
logger_1.log.debug(e.message);
logger_1.log.info(`Assuming that splitting by language is not enabled for the '${apks}' bundle ` +
`and returning the main apk instead`);
return await this.extractBaseApk(apks);
}
}
const defaultLanguages = ['en', 'en_us'];
for (const lang of defaultLanguages) {
try {
return await extractFromApks(apks, ['splits', LANGUAGE_APK(lang)]);
}
catch { }
}
logger_1.log.info(`Cannot find any split apk for the default languages ${JSON.stringify(defaultLanguages)}. ` +
`Returning the main apk instead.`);
return await this.extractBaseApk(apks);
}
/**
*
* @param output
* @returns
*/
function isTestPackageOnlyError(output) {
return /\[INSTALL_FAILED_TEST_ONLY\]/.test(output);
}
//# sourceMappingURL=apks-utils.js.map