From 6fbd54d69e9f60b932d9f0b8b85dd071b37fee62 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Tue, 14 Mar 2017 19:28:16 +0200 Subject: [PATCH 1/2] Fix getting info for Android tools Due to changes in Android SDK, we have to update the checks in CLI. While gathering system information, we check the android executable, which is no longer returning correct results. In order to fix this, we rely on the emulator executable, which is the real thing we need as it is the one that allows us to work with Android Emulators. Fix sys-info checks and get correct path to emulator according to latest changes. --- declarations.d.ts | 6 ++--- definitions/mobile.d.ts | 1 + mobile/android/android-emulator-services.ts | 22 +++++++++++++---- sys-info-base.ts | 22 ++++++++--------- test/unit-tests/sys-info-base.ts | 26 ++++++++++++--------- 5 files changed, 48 insertions(+), 29 deletions(-) diff --git a/declarations.d.ts b/declarations.d.ts index 787fce10..48e1c8e8 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -852,8 +852,8 @@ interface ISysInfoData { adbVer: string; /** Whether iTunes is installed on the machine */ itunesInstalled: boolean; - /** Whether `android` executable can be run */ - androidInstalled: boolean; + /** Whether `emulator` executable can be run */ + emulatorInstalled: boolean; /** mono version, relevant on Mac only **/ monoVer: string; /** git version string, as returned by `git --version` **/ @@ -875,7 +875,7 @@ interface ISysInfo { * @param {any} androidToolsInfo Defines paths to adb and android executables. * @return {Promise} Object containing information for current system. */ - getSysInfo(pathToPackageJson: string, androidToolsInfo?: { pathToAdb: string, pathToAndroid: string }): Promise; + getSysInfo(pathToPackageJson: string, androidToolsInfo?: { pathToAdb: string }): Promise; /** Returns Java version. **/ getJavaVersion(): Promise; diff --git a/definitions/mobile.d.ts b/definitions/mobile.d.ts index 52bf303e..988ca2a0 100644 --- a/definitions/mobile.d.ts +++ b/definitions/mobile.d.ts @@ -491,6 +491,7 @@ declare module Mobile { interface IAndroidEmulatorServices extends IEmulatorPlatformServices { getAllRunningEmulators(): Promise; + pathToEmulatorExecutable: string; } interface IiSimDevice { diff --git a/mobile/android/android-emulator-services.ts b/mobile/android/android-emulator-services.ts index 9e029f1b..644d4392 100644 --- a/mobile/android/android-emulator-services.ts +++ b/mobile/android/android-emulator-services.ts @@ -44,11 +44,25 @@ class AndroidEmulatorServices implements Mobile.IAndroidEmulatorServices { this.adbFilePath = await this.$staticConfig.getAdbFilePath(); } - private get pathToEmulatorExecutable(): string { + public get pathToEmulatorExecutable(): string { if (!this._pathToEmulatorExecutable) { - let androidHome = process.env.ANDROID_HOME; - let emulatorExecutableName = "emulator"; - this._pathToEmulatorExecutable = androidHome ? path.join(androidHome, "tools", emulatorExecutableName) : emulatorExecutableName; + const androidHome = process.env.ANDROID_HOME; + const emulatorExecutableName = "emulator"; + + this._pathToEmulatorExecutable = emulatorExecutableName; + + if (androidHome) { + // Check https://developer.android.com/studio/releases/sdk-tools.html (25.3.0) + // Since this version of SDK tools, the emulator is a separate package. + // However the emulator executable still exists in the "tools" dir. + const pathToEmulatorFromAndroidStudio = path.join(androidHome, emulatorExecutableName, emulatorExecutableName); + + if (this.$fs.exists(pathToEmulatorFromAndroidStudio)) { + this._pathToEmulatorExecutable = pathToEmulatorFromAndroidStudio; + } else { + this._pathToEmulatorExecutable = path.join(androidHome, "tools", emulatorExecutableName); + } + } } return this._pathToEmulatorExecutable; diff --git a/sys-info-base.ts b/sys-info-base.ts index 21333dbb..d97a20ef 100644 --- a/sys-info-base.ts +++ b/sys-info-base.ts @@ -8,7 +8,8 @@ export class SysInfoBase implements ISysInfo { protected $hostInfo: IHostInfo, protected $iTunesValidator: Mobile.IiTunesValidator, protected $logger: ILogger, - protected $winreg: IWinReg) { } + protected $winreg: IWinReg, + protected $androidEmulatorServices: Mobile.IAndroidEmulatorServices) { } private monoVerRegExp = /version (\d+[.]\d+[.]\d+) /gm; private sysInfoCache: ISysInfoData = undefined; @@ -134,7 +135,7 @@ export class SysInfoBase implements ISysInfo { return this.cocoapodVersionCache; } - public async getSysInfo(pathToPackageJson: string, androidToolsInfo?: { pathToAdb: string, pathToAndroid: string }): Promise { + public async getSysInfo(pathToPackageJson: string, androidToolsInfo?: { pathToAdb: string }): Promise { if (!this.sysInfoCache) { let res: ISysInfoData = Object.create(null); let procOutput: string; @@ -167,7 +168,6 @@ export class SysInfoBase implements ISysInfo { res.cocoapodVer = await this.getCocoapodVersion(); let pathToAdb = androidToolsInfo ? androidToolsInfo.pathToAdb : "adb"; - let pathToAndroid = androidToolsInfo ? androidToolsInfo.pathToAndroid : "android"; if (!androidToolsInfo) { this.$logger.trace("'adb' and 'android' will be checked from PATH environment variable."); @@ -176,7 +176,7 @@ export class SysInfoBase implements ISysInfo { procOutput = await this.exec(`${quoteString(pathToAdb)} version`); res.adbVer = procOutput ? procOutput.split(os.EOL)[0] : null; - res.androidInstalled = await this.checkAndroid(pathToAndroid); + res.emulatorInstalled = await this.checkEmulator(); procOutput = await this.exec("mono --version"); if (!!procOutput) { @@ -212,16 +212,16 @@ export class SysInfoBase implements ISysInfo { return null; } - // `android -h` returns exit code 1 on successful invocation (Mac OS X for now, possibly Linux). Therefore, we cannot use $childProcess - private async checkAndroid(pathToAndroid: string): Promise { + private async checkEmulator(): Promise { let result = false; try { - if (pathToAndroid) { - let androidChildProcess = await this.$childProcess.spawnFromEvent(pathToAndroid, ["-h"], "close", {}, { throwError: false }); - result = androidChildProcess && androidChildProcess.stdout && _.includes(androidChildProcess.stdout, "android"); - } + // emulator -help exits with code 1 on Windows, so we should parse the output. + // First line of it should be: + // Android Emulator usage: emulator [options] [-qemu args] + const emulatorHelp = await this.$childProcess.spawnFromEvent(this.$androidEmulatorServices.pathToEmulatorExecutable, ["-help"], "close", {}, { throwError: false }); + result = emulatorHelp.stdout.indexOf("usage: emulator") !== -1; } catch (err) { - this.$logger.trace(`Error while checking is ${pathToAndroid} installed. Error is: ${err.messge}`); + this.$logger.trace(`Error while checking is emulator installed. Error is: ${err.messge}`); } return result; diff --git a/test/unit-tests/sys-info-base.ts b/test/unit-tests/sys-info-base.ts index f575dfd8..fda24543 100644 --- a/test/unit-tests/sys-info-base.ts +++ b/test/unit-tests/sys-info-base.ts @@ -24,7 +24,7 @@ interface IChildProcessResults { nodeGypVersion: IChildProcessResultDescription; xCodeVersion: IChildProcessResultDescription; adbVersion: IChildProcessResultDescription; - androidInstalled: IChildProcessResultDescription; + emulatorInstalled: IChildProcessResultDescription; monoVersion: IChildProcessResultDescription; gradleVersion: IChildProcessResultDescription; gitVersion: IChildProcessResultDescription; @@ -50,7 +50,7 @@ function createChildProcessResults(childProcessResult: IChildProcessResults): ID "pod --version": childProcessResult.podVersion, '"adb" version': childProcessResult.adbVersion, "'adb' version": childProcessResult.adbVersion, // for Mac and Linux - 'android': childProcessResult.androidInstalled, + 'emulator': childProcessResult.emulatorInstalled, "mono --version": childProcessResult.monoVersion, "git --version": childProcessResult.gitVersion, "gradle -v": childProcessResult.gradleVersion @@ -93,6 +93,10 @@ function createTestInjector(childProcessResult: IChildProcessResults, hostInfoDa injector.register("sysInfoBase", SysInfoBase); + injector.register("androidEmulatorServices", { + pathToEmulatorExecutable: "emulator" + }); + return injector; } @@ -116,7 +120,7 @@ describe("sysInfoBase", () => { nodeGypVersion: { result: "2.0.0" }, xCodeVersion: { result: "6.4.0" }, adbVersion: { result: "Android Debug Bridge version 1.0.32" }, - androidInstalled: { result: { stdout: "android" } }, + emulatorInstalled: { result: { stdout: "Android Emulator usage: emulator [options] [-qemu args]" } }, monoVersion: { result: "version 1.0.6 " }, gradleVersion: { result: "Gradle 2.8" }, gitVersion: { result: "git version 1.9.5" }, @@ -134,7 +138,7 @@ describe("sysInfoBase", () => { assert.deepEqual(result.javacVersion, "1.8.0_60"); assert.deepEqual(result.nodeGypVer, childProcessResult.nodeGypVersion.result); assert.deepEqual(result.adbVer, childProcessResult.adbVersion.result); - assert.deepEqual(result.androidInstalled, true); + assert.deepEqual(result.emulatorInstalled, true); assert.deepEqual(result.monoVer, "1.0.6"); assert.deepEqual(result.gradleVer, "2.8"); assert.deepEqual(result.gitVer, "1.9.5"); @@ -212,7 +216,7 @@ describe("sysInfoBase", () => { nodeGypVersion: { shouldThrowError: true }, xCodeVersion: { shouldThrowError: true }, adbVersion: { shouldThrowError: true }, - androidInstalled: { shouldThrowError: true }, + emulatorInstalled: { shouldThrowError: true }, monoVersion: { shouldThrowError: true }, gradleVersion: { shouldThrowError: true }, gitVersion: { shouldThrowError: true }, @@ -221,18 +225,18 @@ describe("sysInfoBase", () => { }); describe("when android info is incorrect", () => { - it("pathToAdb and pathToAndroid are null", async () => { + it("pathToAdb is null", async () => { childProcessResult.adbVersion = { result: null }; - childProcessResult.androidInstalled = { - result: false + childProcessResult.emulatorInstalled = { + result: null }; testInjector = createTestInjector(childProcessResult, { isWindows: false, isDarwin: false, dotNetVersion: "4.5.1" }, null); sysInfoBase = testInjector.resolve("sysInfoBase"); - let result = await sysInfoBase.getSysInfo(toolsPackageJson, { pathToAdb: null, pathToAndroid: null }); + let result = await sysInfoBase.getSysInfo(toolsPackageJson, { pathToAdb: null }); assert.deepEqual(result.adbVer, null); - assert.deepEqual(result.androidInstalled, false); + assert.deepEqual(result.emulatorInstalled, false); }); }); @@ -246,7 +250,7 @@ describe("sysInfoBase", () => { assert.deepEqual(result.nodeGypVer, null); assert.deepEqual(result.xcodeVer, null); assert.deepEqual(result.adbVer, null); - assert.deepEqual(result.androidInstalled, false); + assert.deepEqual(result.emulatorInstalled, false); assert.deepEqual(result.monoVer, null); assert.deepEqual(result.gradleVer, null); assert.deepEqual(result.gitVer, null); From f2929e7efd9d2bc56152cc5e5b460dfb0577b9c9 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Wed, 15 Mar 2017 23:16:13 +0200 Subject: [PATCH 2/2] Expose methods for getting info about AVDs AndroidEmulatorServices knows how to get information about Android Virtual Devices. Make this methods public, so they can be called from other services which need this information. Fix getting path to emulator executable on Windows - it has `.exe` and the check if it exists was always false. --- definitions/mobile.d.ts | 2 + mobile/android/android-emulator-services.ts | 52 ++++++++++++--------- sys-info-base.ts | 16 +++---- test/unit-tests/sys-info-base.ts | 16 +++++-- 4 files changed, 49 insertions(+), 37 deletions(-) diff --git a/definitions/mobile.d.ts b/definitions/mobile.d.ts index 988ca2a0..d0880298 100644 --- a/definitions/mobile.d.ts +++ b/definitions/mobile.d.ts @@ -492,6 +492,8 @@ declare module Mobile { interface IAndroidEmulatorServices extends IEmulatorPlatformServices { getAllRunningEmulators(): Promise; pathToEmulatorExecutable: string; + getInfoFromAvd(avdName: string): Mobile.IAvdInfo; + getAvds(): string[]; } interface IiSimDevice { diff --git a/mobile/android/android-emulator-services.ts b/mobile/android/android-emulator-services.ts index 644d4392..3c5228e7 100644 --- a/mobile/android/android-emulator-services.ts +++ b/mobile/android/android-emulator-services.ts @@ -35,7 +35,8 @@ class AndroidEmulatorServices implements Mobile.IAndroidEmulatorServices { private $logcatHelper: Mobile.ILogcatHelper, private $options: ICommonOptions, private $utils: IUtils, - private $injector: IInjector) { + private $injector: IInjector, + private $hostInfo: IHostInfo) { iconv.extendNodeEncodings(); } @@ -55,9 +56,11 @@ class AndroidEmulatorServices implements Mobile.IAndroidEmulatorServices { // Check https://developer.android.com/studio/releases/sdk-tools.html (25.3.0) // Since this version of SDK tools, the emulator is a separate package. // However the emulator executable still exists in the "tools" dir. - const pathToEmulatorFromAndroidStudio = path.join(androidHome, emulatorExecutableName, emulatorExecutableName); + const pathToEmulatorFromAndroidStudio = path.join(androidHome, emulatorExecutableName, emulatorExecutableName); - if (this.$fs.exists(pathToEmulatorFromAndroidStudio)) { + const realFilePath = this.$hostInfo.isWindows ? `${pathToEmulatorFromAndroidStudio}.exe` : pathToEmulatorFromAndroidStudio; + + if (this.$fs.exists(realFilePath)) { this._pathToEmulatorExecutable = pathToEmulatorFromAndroidStudio; } else { this._pathToEmulatorExecutable = path.join(androidHome, "tools", emulatorExecutableName); @@ -385,26 +388,39 @@ class AndroidEmulatorServices implements Mobile.IAndroidEmulatorServices { } } + public getInfoFromAvd(avdName: string): Mobile.IAvdInfo { + let iniFile = path.join(this.avdDir, avdName + ".ini"), + avdInfo: Mobile.IAvdInfo = this.parseAvdFile(avdName, iniFile); + + if (avdInfo.path && this.$fs.exists(avdInfo.path)) { + iniFile = path.join(avdInfo.path, "config.ini"); + avdInfo = this.parseAvdFile(avdName, iniFile, avdInfo); + } + + return avdInfo; + } + + public getAvds(): string[] { + let result: string[] = []; + if (this.$fs.exists(this.avdDir)) { + let entries = this.$fs.readDirectory(this.avdDir); + result = _.filter(entries, (e: string) => e.match(AndroidEmulatorServices.INI_FILES_MASK) !== null) + .map((e) => e.match(AndroidEmulatorServices.INI_FILES_MASK)[1]); + } + return result; + } + private getBestFit(): string { let minVersion = this.$emulatorSettingsService.minVersion; let best = _(this.getAvds()) .map(avd => this.getInfoFromAvd(avd)) + .filter(avd => !!avd) .maxBy(avd => avd.targetNum); return (best && best.targetNum >= minVersion) ? best.name : null; } - private getInfoFromAvd(avdName: string): Mobile.IAvdInfo { - let iniFile = path.join(this.avdDir, avdName + ".ini"); - let avdInfo: Mobile.IAvdInfo = this.parseAvdFile(avdName, iniFile); - if (avdInfo.path && this.$fs.exists(avdInfo.path)) { - iniFile = path.join(avdInfo.path, "config.ini"); - avdInfo = this.parseAvdFile(avdName, iniFile, avdInfo); - } - return avdInfo; - } - private parseAvdFile(avdName: string, avdFileName: string, avdInfo?: Mobile.IAvdInfo): Mobile.IAvdInfo { if (!this.$fs.exists(avdFileName)) { return null; @@ -479,16 +495,6 @@ class AndroidEmulatorServices implements Mobile.IAndroidEmulatorServices { return path.join(this.androidHomeDir, AndroidEmulatorServices.AVD_DIR_NAME); } - private getAvds(): string[] { - let result: string[] = []; - if (this.$fs.exists(this.avdDir)) { - let entries = this.$fs.readDirectory(this.avdDir); - result = _.filter(entries, (e: string) => e.match(AndroidEmulatorServices.INI_FILES_MASK) !== null) - .map((e) => e.match(AndroidEmulatorServices.INI_FILES_MASK)[1]); - } - return result; - } - private async waitForEmulatorBootToComplete(emulatorId: string): Promise { this.$logger.printInfoMessageOnSameLine("Waiting for emulator device initialization..."); diff --git a/sys-info-base.ts b/sys-info-base.ts index d97a20ef..da532c3f 100644 --- a/sys-info-base.ts +++ b/sys-info-base.ts @@ -213,16 +213,12 @@ export class SysInfoBase implements ISysInfo { } private async checkEmulator(): Promise { - let result = false; - try { - // emulator -help exits with code 1 on Windows, so we should parse the output. - // First line of it should be: - // Android Emulator usage: emulator [options] [-qemu args] - const emulatorHelp = await this.$childProcess.spawnFromEvent(this.$androidEmulatorServices.pathToEmulatorExecutable, ["-help"], "close", {}, { throwError: false }); - result = emulatorHelp.stdout.indexOf("usage: emulator") !== -1; - } catch (err) { - this.$logger.trace(`Error while checking is emulator installed. Error is: ${err.messge}`); - } + // emulator -help exits with code 1 on Windows, so we should parse the output. + // First line of it should be: + // Android Emulator usage: emulator [options] [-qemu args] + const emulatorHelp = await this.$childProcess.spawnFromEvent(this.$androidEmulatorServices.pathToEmulatorExecutable, ["-help"], "close", {}, { throwError: false }); + const result = !!(emulatorHelp && emulatorHelp.stdout && emulatorHelp.stdout.indexOf("usage: emulator") !== -1); + this.$logger.trace(`The result of checking is Android Emulator installed is:${os.EOL}- stdout: ${emulatorHelp && emulatorHelp.stdout}${os.EOL}- stderr: ${emulatorHelp && emulatorHelp.stderr}`); return result; } diff --git a/test/unit-tests/sys-info-base.ts b/test/unit-tests/sys-info-base.ts index fda24543..9bc6df24 100644 --- a/test/unit-tests/sys-info-base.ts +++ b/test/unit-tests/sys-info-base.ts @@ -31,9 +31,17 @@ interface IChildProcessResults { podVersion: IChildProcessResultDescription; } -function getResultFromChildProcess(childProcessResultDescription: IChildProcessResultDescription): any { +function getResultFromChildProcess(childProcessResultDescription: IChildProcessResultDescription, spawnFromEventOpts?: { throwError: boolean }): any { if (childProcessResultDescription.shouldThrowError) { - throw new Error("This one throws error."); + if (spawnFromEventOpts && !spawnFromEventOpts.throwError) { + return { + stderr: "This one throws error.", + code: 1, + stdout: null + }; + } else { + throw new Error("This one throws error."); + } } return childProcessResultDescription.result; @@ -65,8 +73,8 @@ function createTestInjector(childProcessResult: IChildProcessResults, hostInfoDa return getResultFromChildProcess(childProcessResultDictionary[command]); }, - spawnFromEvent: (command: string, args: string[], event: string) => { - return getResultFromChildProcess(childProcessResultDictionary[command]); + spawnFromEvent: (command: string, args: string[], event: string, opts: any, spawnFromEventOpts?: { throwError: boolean }) => { + return getResultFromChildProcess(childProcessResultDictionary[command], spawnFromEventOpts); } });