microsoft/vscode-react-native
Publicmirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable
src/extension/ios/iOSTargetManager.ts
287lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. |
| 3 | |
| 4 | import * as nls from "vscode-nls"; |
| 5 | import { QuickPickOptions, window } from "vscode"; |
| 6 | import { ChildProcess } from "../../common/node/childProcess"; |
| 7 | import { PromiseUtil } from "../../common/node/promise"; |
| 8 | import { IDebuggableMobileTarget, MobileTarget } from "../mobileTarget"; |
| 9 | import { MobileTargetManager } from "../mobileTargetManager"; |
| 10 | import { OutputChannelLogger } from "../log/OutputChannelLogger"; |
| 11 | import { TargetType } from "../generalPlatform"; |
| 12 | |
| 13 | nls.config({ |
| 14 | messageFormat: nls.MessageFormat.bundle, |
| 15 | bundleFormat: nls.BundleFormat.standalone, |
| 16 | })(); |
| 17 | const localize = nls.loadMessageBundle(); |
| 18 | |
| 19 | export interface IDebuggableIOSTarget extends IDebuggableMobileTarget { |
| 20 | name: string; |
| 21 | system: string; |
| 22 | } |
| 23 | |
| 24 | export class IOSTarget extends MobileTarget implements IDebuggableIOSTarget { |
| 25 | protected _system: string; |
| 26 | protected _name: string; |
| 27 | |
| 28 | public static fromInterface(obj: IDebuggableIOSTarget): IOSTarget { |
| 29 | return new IOSTarget(obj.isOnline, obj.isVirtualTarget, obj.id, obj.name, obj.system); |
| 30 | } |
| 31 | |
| 32 | constructor( |
| 33 | isOnline: boolean, |
| 34 | isVirtualTarget: boolean, |
| 35 | id: string, |
| 36 | name: string, |
| 37 | system: string, |
| 38 | ) { |
| 39 | super(isOnline, isVirtualTarget, id, name); |
| 40 | this._system = system; |
| 41 | } |
| 42 | |
| 43 | get system(): string { |
| 44 | return this._system; |
| 45 | } |
| 46 | |
| 47 | get name(): string { |
| 48 | return this._name; |
| 49 | } |
| 50 | |
| 51 | set name(value: string) { |
| 52 | this._name = value; |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | export class IOSTargetManager extends MobileTargetManager { |
| 57 | private static readonly XCRUN_COMMAND = "xcrun"; |
| 58 | private static readonly SIMCTL_COMMAND = "simctl"; |
| 59 | private static readonly BOOT_COMMAND = `boot`; |
| 60 | private static readonly SIMULATORS_LIST_COMMAND = `${IOSTargetManager.XCRUN_COMMAND} ${IOSTargetManager.SIMCTL_COMMAND} list devices available --json`; |
| 61 | private static readonly ALL_DEVICES_LIST_COMMAND = `${IOSTargetManager.XCRUN_COMMAND} xctrace list devices`; |
| 62 | private static readonly BOOTED_STATE = "Booted"; |
| 63 | private static readonly SIMULATOR_START_TIMEOUT = 120; |
| 64 | private static readonly ANY_SYSTEM = "AnySystem"; |
| 65 | |
| 66 | private childProcess: ChildProcess = new ChildProcess(); |
| 67 | private logger: OutputChannelLogger = OutputChannelLogger.getChannel( |
| 68 | OutputChannelLogger.MAIN_CHANNEL_NAME, |
| 69 | true, |
| 70 | ); |
| 71 | protected targets?: IDebuggableIOSTarget[]; |
| 72 | |
| 73 | public async collectTargets(targetType?: TargetType): Promise<void> { |
| 74 | this.targets = []; |
| 75 | if (targetType === undefined || targetType === TargetType.Simulator) { |
| 76 | const simulators = JSON.parse( |
| 77 | await this.childProcess.execToString(`${IOSTargetManager.SIMULATORS_LIST_COMMAND}`), |
| 78 | ); |
| 79 | Object.keys(simulators.devices).forEach(rawSystem => { |
| 80 | const temp = rawSystem.split(".").slice(-1)[0].split("-"); // "com.apple.CoreSimulator.SimRuntime.iOS-11-4" -> ["iOS", "11", "4"] |
| 81 | const system = `${temp[0]} ${temp.slice(1).join(".")}`; // ["iOS", "11", "4"] -> iOS 11.4 |
| 82 | simulators.devices[rawSystem].forEach((device: any) => { |
| 83 | // Now we support selection only for iOS system |
| 84 | if (system.includes("iOS")) { |
| 85 | this.targets?.push({ |
| 86 | id: device.udid, |
| 87 | name: device.name, |
| 88 | system, |
| 89 | isVirtualTarget: true, |
| 90 | isOnline: device.state === IOSTargetManager.BOOTED_STATE, |
| 91 | }); |
| 92 | } |
| 93 | }); |
| 94 | }); |
| 95 | } |
| 96 | |
| 97 | if (targetType === undefined || targetType === TargetType.Device) { |
| 98 | const allDevicesOutput = await this.childProcess.execToString( |
| 99 | `${IOSTargetManager.ALL_DEVICES_LIST_COMMAND}`, |
| 100 | ); |
| 101 | // Output example: |
| 102 | // == Devices == |
| 103 | // sierra (EFDAAD01-E1A3-5F00-A357-665B501D5520) |
| 104 | // My iPhone (14.4.2) (33n546e591e707bd64c718bfc1bf3e8b7c16bfc9) |
| 105 | // |
| 106 | // == Simulators == |
| 107 | // Apple TV (14.5) (417BDFD8-6E22-4F87-BCAA-19C241AC9548) |
| 108 | // Apple TV 4K (2nd generation) (14.5) (925E6E38-0D7B-45E9-ADE0-89C20779D467) |
| 109 | // ... |
| 110 | const lines = allDevicesOutput |
| 111 | .split("\n") |
| 112 | .map(line => line.trim()) |
| 113 | .filter(line => !!line); |
| 114 | const firstDevicesIndex = lines.indexOf("== Devices ==") + 1; |
| 115 | const lastDevicesIndex = lines.indexOf("== Simulators ==") - 1; |
| 116 | for (let i = firstDevicesIndex; i <= lastDevicesIndex; i++) { |
| 117 | const line = lines[i]; |
| 118 | const params = line |
| 119 | .split(" ") |
| 120 | .map(el => el.trim()) |
| 121 | .filter(el => !!el); |
| 122 | // Add only devices with system version |
| 123 | if ( |
| 124 | params[params.length - 1].match(/\(.+\)/) && |
| 125 | params[params.length - 2].match(/\(.+\)/) |
| 126 | ) { |
| 127 | this.targets.push({ |
| 128 | id: params[params.length - 1].replace(/\(|\)/g, "").trim(), |
| 129 | name: params.slice(0, params.length - 2).join(" "), |
| 130 | system: params[params.length - 2].replace(/\(|\)/g, "").trim(), |
| 131 | isVirtualTarget: false, |
| 132 | isOnline: true, |
| 133 | }); |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | public async selectAndPrepareTarget( |
| 140 | filter?: (el: IDebuggableIOSTarget) => boolean, |
| 141 | ): Promise<IOSTarget | undefined> { |
| 142 | const selectedTarget = await this.startSelection(filter); |
| 143 | if (selectedTarget) { |
| 144 | return !selectedTarget.isOnline && selectedTarget.isVirtualTarget |
| 145 | ? this.launchSimulator(selectedTarget) |
| 146 | : IOSTarget.fromInterface(selectedTarget); |
| 147 | } |
| 148 | return undefined; |
| 149 | } |
| 150 | |
| 151 | public async isVirtualTarget(targetString: string): Promise<boolean> { |
| 152 | try { |
| 153 | if (targetString === TargetType.Device) { |
| 154 | return false; |
| 155 | } else if (targetString === TargetType.Simulator) { |
| 156 | return true; |
| 157 | } |
| 158 | const target = ( |
| 159 | await this.getTargetList( |
| 160 | target => target.id === targetString || target.name === targetString, |
| 161 | ) |
| 162 | )[0]; |
| 163 | if (target) { |
| 164 | return target.isVirtualTarget; |
| 165 | } |
| 166 | throw Error("There is no any target with specified target string"); |
| 167 | } catch { |
| 168 | throw new Error( |
| 169 | localize( |
| 170 | "CouldNotRecognizeTargetType", |
| 171 | "Could not recognize type of the target {0}", |
| 172 | targetString, |
| 173 | ), |
| 174 | ); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | protected async startSelection( |
| 179 | filter?: (el: IDebuggableIOSTarget) => boolean, |
| 180 | ): Promise<IDebuggableIOSTarget | undefined> { |
| 181 | const system = await this.selectSystem(filter); |
| 182 | if (system) { |
| 183 | return (await this.selectTarget( |
| 184 | (el: IDebuggableIOSTarget) => |
| 185 | (filter ? filter(el) : true) && |
| 186 | (system === IOSTargetManager.ANY_SYSTEM ? true : el.system === system), |
| 187 | )) as IDebuggableIOSTarget | undefined; |
| 188 | } |
| 189 | return; |
| 190 | } |
| 191 | |
| 192 | protected async selectSystem( |
| 193 | filter?: (el: IDebuggableIOSTarget) => boolean, |
| 194 | ): Promise<string | undefined> { |
| 195 | const targets = (await this.getTargetList(filter)) as IDebuggableIOSTarget[]; |
| 196 | // If we select only from devices, we should not select system |
| 197 | if (!targets.find(target => target.isVirtualTarget)) { |
| 198 | return IOSTargetManager.ANY_SYSTEM; |
| 199 | } |
| 200 | const names: Set<string> = new Set(targets.map(target => target.system)); |
| 201 | const systemsList = Array.from(names); |
| 202 | let result: string | undefined = systemsList[0]; |
| 203 | if (systemsList.length > 1) { |
| 204 | const quickPickOptions: QuickPickOptions = { |
| 205 | ignoreFocusOut: true, |
| 206 | canPickMany: false, |
| 207 | placeHolder: localize( |
| 208 | "SelectIOSSystemVersion", |
| 209 | "Select system version of iOS target", |
| 210 | ), |
| 211 | }; |
| 212 | result = await window.showQuickPick(systemsList, quickPickOptions); |
| 213 | } |
| 214 | return result?.toString(); |
| 215 | } |
| 216 | |
| 217 | protected async launchSimulator( |
| 218 | virtualTarget: IDebuggableIOSTarget, |
| 219 | ): Promise<IOSTarget | undefined> { |
| 220 | return new Promise<IOSTarget | undefined>((resolve, reject) => { |
| 221 | let emulatorLaunchFailed = false; |
| 222 | const emulatorProcess = this.childProcess.spawn( |
| 223 | IOSTargetManager.XCRUN_COMMAND, |
| 224 | [IOSTargetManager.SIMCTL_COMMAND, IOSTargetManager.BOOT_COMMAND, virtualTarget.id], |
| 225 | { |
| 226 | detached: true, |
| 227 | }, |
| 228 | true, |
| 229 | ); |
| 230 | emulatorProcess.spawnedProcess.unref(); |
| 231 | emulatorProcess.outcome.catch(e => { |
| 232 | emulatorLaunchFailed = true; |
| 233 | this.logger.error( |
| 234 | localize( |
| 235 | "ErrorWhileLaunchingSimulator", |
| 236 | "Error while launching simulator {0} : {1}", |
| 237 | `${virtualTarget.name}(${virtualTarget.id})`, |
| 238 | e, |
| 239 | ), |
| 240 | ); |
| 241 | reject(e); |
| 242 | }); |
| 243 | |
| 244 | const condition = async () => { |
| 245 | if (emulatorLaunchFailed) |
| 246 | throw new Error("iOS simulator launch failed unexpectedly"); |
| 247 | await this.collectTargets(TargetType.Simulator); |
| 248 | const onlineTarget = (await this.getTargetList()).find( |
| 249 | target => target.id === virtualTarget.id && target.isOnline, |
| 250 | ); |
| 251 | return onlineTarget ? true : null; |
| 252 | }; |
| 253 | |
| 254 | void PromiseUtil.waitUntil<boolean>( |
| 255 | condition, |
| 256 | 1000, |
| 257 | IOSTargetManager.SIMULATOR_START_TIMEOUT * 1000, |
| 258 | ).then( |
| 259 | isBooted => { |
| 260 | if (isBooted) { |
| 261 | virtualTarget.isOnline = true; |
| 262 | this.logger.info( |
| 263 | localize( |
| 264 | "SimulatorLaunched", |
| 265 | "Launched simulator {0}", |
| 266 | virtualTarget.name, |
| 267 | ), |
| 268 | ); |
| 269 | resolve(IOSTarget.fromInterface(virtualTarget)); |
| 270 | } else { |
| 271 | reject( |
| 272 | new Error( |
| 273 | `Virtual device launch finished with an exception: ${localize( |
| 274 | "SimulatorStartWarning", |
| 275 | "Could not start the simulator {0} within {1} seconds.", |
| 276 | virtualTarget.name, |
| 277 | IOSTargetManager.SIMULATOR_START_TIMEOUT, |
| 278 | )}`, |
| 279 | ), |
| 280 | ); |
| 281 | } |
| 282 | }, |
| 283 | () => {}, |
| 284 | ); |
| 285 | }); |
| 286 | } |
| 287 | } |
| 288 | |