// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import * as nls from "vscode-nls"; import { MobileTargetManager } from "../mobileTargetManager"; import { ChildProcess } from "../../common/node/childProcess"; import { OutputChannelLogger } from "../log/OutputChannelLogger"; import { IDebuggableMobileTarget, IMobileTarget, MobileTarget } from "../mobileTarget"; import { TargetType } from "../generalPlatform"; import { PromiseUtil } from "../../common/node/promise"; import { InternalErrorCode } from "../../common/error/internalErrorCode"; import { ErrorHelper } from "../../common/error/errorHelper"; import { AdbHelper } from "./adb"; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone, })(); const localize = nls.loadMessageBundle(); export class AndroidTarget extends MobileTarget { public static fromInterface(obj: IDebuggableMobileTarget): AndroidTarget { return new AndroidTarget(obj.isOnline, obj.isVirtualTarget, obj.id, obj.name); } constructor(isOnline: boolean, isVirtualTarget: boolean, id: string, name?: string) { super(isOnline, isVirtualTarget, id, name ? name : id); } } export class AndroidTargetManager extends MobileTargetManager { private static readonly EMULATOR_COMMAND = "emulator"; private static readonly EMULATOR_AVD_START_COMMAND = "-avd"; private static readonly EMULATOR_START_TIMEOUT = 120; private logger: OutputChannelLogger; private adbHelper: AdbHelper; private childProcess: ChildProcess; constructor(adbHelper: AdbHelper) { super(); this.adbHelper = adbHelper; this.logger = OutputChannelLogger.getChannel(OutputChannelLogger.MAIN_CHANNEL_NAME, true); this.childProcess = new ChildProcess(); } public async isVirtualTarget(target: string): Promise { try { if (target === TargetType.Device) { return false; } else if ( target === TargetType.Simulator || target.match(AdbHelper.AndroidSDKEmulatorPattern) ) { return true; } const onlineTarget = await this.adbHelper.findOnlineTargetById(target); if (onlineTarget) { return onlineTarget.isVirtualTarget; } else if ((await this.adbHelper.getAvdsNames()).includes(target)) { return true; } throw new Error("There is no such target"); } catch (error) { throw ErrorHelper.getNestedError( error as Error, InternalErrorCode.CouldNotRecognizeTargetType, target, ); } } public async selectAndPrepareTarget( filter?: (el: IMobileTarget) => boolean, ): Promise { const selectedTarget = await this.startSelection(filter); if (selectedTarget) { if (!selectedTarget.isOnline && selectedTarget.isVirtualTarget) { return this.launchSimulator(selectedTarget); } if (selectedTarget.id) { return AndroidTarget.fromInterface(selectedTarget); } } return undefined; } public async collectTargets(targetType?: TargetType): Promise { const targetList: IMobileTarget[] = []; const collectSimulators = !targetType || targetType === TargetType.Simulator; const collectDevices = !targetType || targetType === TargetType.Device; try { if (collectSimulators) { const emulatorsNames: string[] = await this.adbHelper.getAvdsNames(); targetList.push( ...emulatorsNames.map(name => ({ name, isOnline: false, isVirtualTarget: true, })), ); } } catch (error) { // We throw an exception only if the target type is explicitly specified, // otherwise we collect only those targets that we can collect if (targetType === TargetType.Simulator) { throw error; } this.logger.warning( localize( "CouldNotUseEmulators", "An error occurred while trying to get installed emulators: {0}\nContinue using only online targets", error instanceof Error ? (error as Error).message : (error as Error).toString(), ), ); } const onlineTargets = await this.adbHelper.getOnlineTargets(); for (const device of onlineTargets) { if (device.isVirtualTarget && collectSimulators) { const avdName = await this.adbHelper.getAvdNameById(device.id); const emulatorTarget = targetList.find(target => target.name === avdName); if (emulatorTarget) { emulatorTarget.isOnline = true; emulatorTarget.id = device.id; } } else if (!device.isVirtualTarget && collectDevices) { targetList.push({ id: device.id, isOnline: true, isVirtualTarget: false }); } } this.targets = targetList; } protected async startSelection( filter?: (el: IMobileTarget) => boolean, ): Promise { return this.selectTarget(filter); } protected async launchSimulator(emulatorTarget: IMobileTarget): Promise { return new Promise((resolve, reject) => { let emulatorLaunchFailed = false; const emulatorProcess = this.childProcess.spawn( AndroidTargetManager.EMULATOR_COMMAND, [AndroidTargetManager.EMULATOR_AVD_START_COMMAND, emulatorTarget.name as string], { detached: true, }, true, ); emulatorProcess.outcome.catch(error => { emulatorLaunchFailed = true; if ( process.platform == "win32" && process.env.SESSIONNAME && process.env.SESSIONNAME.toLowerCase().includes("rdp-tcp") ) { this.logger.warning( localize( "RDPEmulatorWarning", "Android emulator was launched from the Windows RDP session, this might lead to failures.", ), ); } reject( new Error(`Virtual device launch finished with an exception: ${String(error)}`), ); }); emulatorProcess.spawnedProcess.unref(); const condition = async () => { if (emulatorLaunchFailed) throw new Error("Android emulator launch failed unexpectedly"); const connectedDevices = await this.adbHelper.getOnlineTargets(); for (const target of connectedDevices) { const onlineAvdName = await this.adbHelper.getAvdNameById(target.id); if (onlineAvdName === emulatorTarget.name) { return target.id; } } return null; }; void PromiseUtil.waitUntil( condition, 1000, AndroidTargetManager.EMULATOR_START_TIMEOUT * 1000, ).then( emulatorId => { if (emulatorId) { emulatorTarget.id = emulatorId; emulatorTarget.isOnline = true; this.logger.info( localize( "EmulatorLaunched", "Launched Android emulator {0}", emulatorTarget.name, ), ); resolve( AndroidTarget.fromInterface(emulatorTarget), ); } else { reject( new Error( `Virtual device launch finished with an exception: ${localize( "EmulatorStartWarning", "Could not start the emulator {0} within {1} seconds.", emulatorTarget.name, AndroidTargetManager.EMULATOR_START_TIMEOUT, )}`, ), ); } }, () => {}, ); }); } }