// 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 { QuickPickOptions, window } from "vscode"; import { ChildProcess } from "../../common/node/childProcess"; import { PromiseUtil } from "../../common/node/promise"; import { IDebuggableMobileTarget, IMobileTarget, MobileTarget } from "../mobileTarget"; import { MobileTargetManager } from "../mobileTargetManager"; import { OutputChannelLogger } from "../log/OutputChannelLogger"; import { TargetType } from "../generalPlatform"; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone, })(); const localize = nls.loadMessageBundle(); export interface IDebuggableIOSTarget extends IDebuggableMobileTarget { name: string; system: string; } export class IOSTarget extends MobileTarget implements IDebuggableIOSTarget { protected _system: string; protected _name!: string; public static fromInterface(obj: IDebuggableIOSTarget): IOSTarget { return new IOSTarget(obj.isOnline, obj.isVirtualTarget, obj.id, obj.name, obj.system); } constructor( isOnline: boolean, isVirtualTarget: boolean, id: string, name: string, system: string, ) { super(isOnline, isVirtualTarget, id, name); this._system = system; } get system(): string { return this._system; } get name(): string { return this._name; } set name(value: string) { this._name = value; } } export class IOSTargetManager extends MobileTargetManager { private static readonly XCRUN_COMMAND = "xcrun"; private static readonly SIMCTL_COMMAND = "simctl"; private static readonly BOOT_COMMAND = `boot`; private static readonly SIMULATORS_LIST_COMMAND = `${IOSTargetManager.XCRUN_COMMAND} ${IOSTargetManager.SIMCTL_COMMAND} list devices available --json`; private static readonly ALL_DEVICES_LIST_COMMAND = `${IOSTargetManager.XCRUN_COMMAND} xctrace list devices`; private static readonly BOOTED_STATE = "Booted"; private static readonly SIMULATOR_START_TIMEOUT = 120; private static readonly ANY_SYSTEM = "AnySystem"; private childProcess: ChildProcess = new ChildProcess(); private logger: OutputChannelLogger = OutputChannelLogger.getChannel( OutputChannelLogger.MAIN_CHANNEL_NAME, true, ); protected targets?: IDebuggableIOSTarget[]; public async collectTargets(targetType?: TargetType): Promise { this.targets = []; if (targetType === undefined || targetType === TargetType.Simulator) { const simulators = JSON.parse( await this.childProcess.execToString(`${IOSTargetManager.SIMULATORS_LIST_COMMAND}`), ); Object.keys(simulators.devices).forEach(rawSystem => { const temp = rawSystem.split(".").slice(-1)[0].split("-"); // "com.apple.CoreSimulator.SimRuntime.iOS-11-4" -> ["iOS", "11", "4"] const system = `${temp[0]} ${temp.slice(1).join(".")}`; // ["iOS", "11", "4"] -> iOS 11.4 simulators.devices[rawSystem].forEach((device: any) => { // Now we support selection only for iOS system if (system.includes("iOS")) { this.targets?.push({ id: device.udid, name: device.name, system, isVirtualTarget: true, isOnline: device.state === IOSTargetManager.BOOTED_STATE, }); } }); }); } if (targetType === undefined || targetType === TargetType.Device) { const allDevicesOutput = await this.childProcess.execToString( `${IOSTargetManager.ALL_DEVICES_LIST_COMMAND}`, ); // Output example: // == Devices == // sierra (EFDAAD01-E1A3-5F00-A357-665B501D5520) // My iPhone (14.4.2) (33n546e591e707bd64c718bfc1bf3e8b7c16bfc9) // // == Simulators == // Apple TV (14.5) (417BDFD8-6E22-4F87-BCAA-19C241AC9548) // Apple TV 4K (2nd generation) (14.5) (925E6E38-0D7B-45E9-ADE0-89C20779D467) // ... const lines = allDevicesOutput .split("\n") .map(line => line.trim()) .filter(line => !!line); const firstDevicesIndex = lines.indexOf("== Devices ==") + 1; const lastDevicesIndex = lines.indexOf("== Simulators ==") - 1; for (let i = firstDevicesIndex; i <= lastDevicesIndex; i++) { const line = lines[i]; const params = line .split(" ") .map(el => el.trim()) .filter(el => !!el); // Add only devices with system version if ( params[params.length - 1].match(/\(.+\)/) && params[params.length - 2].match(/\(.+\)/) ) { this.targets.push({ id: params[params.length - 1].replace(/\(|\)/g, "").trim(), name: params.slice(0, params.length - 2).join(" "), system: params[params.length - 2].replace(/\(|\)/g, "").trim(), isVirtualTarget: false, isOnline: true, }); } } } } public async selectAndPrepareTarget( filter?: (el: IDebuggableIOSTarget) => boolean, ): Promise { const selectedTarget = await this.startSelection(filter); if (selectedTarget) { return !selectedTarget.isOnline && selectedTarget.isVirtualTarget ? this.launchSimulator(selectedTarget) : IOSTarget.fromInterface(selectedTarget); } return undefined; } public async isVirtualTarget(targetString: string): Promise { try { if (targetString === TargetType.Device) { return false; } else if (targetString === TargetType.Simulator) { return true; } const target = ( await this.getTargetList( target => target.id === targetString || target.name === targetString, ) )[0]; if (target) { return target.isVirtualTarget; } throw Error("There is no any target with specified target string"); } catch { throw new Error( localize( "CouldNotRecognizeTargetType", "Could not recognize type of the target {0}", targetString, ), ); } } protected async startSelection( filter?: (el: IDebuggableIOSTarget) => boolean, ): Promise { const system = await this.selectSystem(filter); if (system) { return (await this.selectTarget( (el: IMobileTarget) => (filter ? filter(el as IDebuggableIOSTarget) : true) && (system === IOSTargetManager.ANY_SYSTEM ? true : (el as IDebuggableIOSTarget).system === system), )) as IDebuggableIOSTarget | undefined; } return; } protected async selectSystem( filter?: (el: IDebuggableIOSTarget) => boolean, ): Promise { const targets = (await this.getTargetList( filter as ((el: IMobileTarget) => boolean) | undefined, )) as IDebuggableIOSTarget[]; // If we select only from devices, we should not select system if (!targets.find(target => target.isVirtualTarget)) { return IOSTargetManager.ANY_SYSTEM; } const names: Set = new Set(targets.map(target => target.system)); const systemsList = Array.from(names); let result: string | undefined = systemsList[0]; if (systemsList.length > 1) { const quickPickOptions: QuickPickOptions = { ignoreFocusOut: true, canPickMany: false, placeHolder: localize( "SelectIOSSystemVersion", "Select system version of iOS target", ), }; result = await window.showQuickPick(systemsList, quickPickOptions); } return result?.toString(); } protected async launchSimulator( virtualTarget: IDebuggableIOSTarget, ): Promise { return new Promise((resolve, reject) => { let emulatorLaunchFailed = false; const emulatorProcess = this.childProcess.spawn( IOSTargetManager.XCRUN_COMMAND, [IOSTargetManager.SIMCTL_COMMAND, IOSTargetManager.BOOT_COMMAND, virtualTarget.id], { detached: true, }, true, ); emulatorProcess.spawnedProcess.unref(); emulatorProcess.outcome.catch(e => { emulatorLaunchFailed = true; this.logger.error( localize( "ErrorWhileLaunchingSimulator", "Error while launching simulator {0} : {1}", `${virtualTarget.name}(${virtualTarget.id})`, e, ), ); reject(e); }); const condition = async () => { if (emulatorLaunchFailed) throw new Error("iOS simulator launch failed unexpectedly"); await this.collectTargets(TargetType.Simulator); const onlineTarget = (await this.getTargetList()).find( target => target.id === virtualTarget.id && target.isOnline, ); return onlineTarget ? true : null; }; void PromiseUtil.waitUntil( condition, 1000, IOSTargetManager.SIMULATOR_START_TIMEOUT * 1000, ).then( isBooted => { if (isBooted) { virtualTarget.isOnline = true; this.logger.info( localize( "SimulatorLaunched", "Launched simulator {0}", virtualTarget.name, ), ); resolve(IOSTarget.fromInterface(virtualTarget)); } else { reject( new Error( `Virtual device launch finished with an exception: ${localize( "SimulatorStartWarning", "Could not start the simulator {0} within {1} seconds.", virtualTarget.name, IOSTargetManager.SIMULATOR_START_TIMEOUT, )}`, ), ); } }, () => {}, ); }); } }