microsoft/vscode-react-native
Publicmirrored from https://github.com/microsoft/vscode-react-nativeAvailable
src/extension/ios/iOSTargetManager.ts
291lines · modeblame
4cd25962JiglioNero4 years ago | 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. | |
| 3 | | |
09f6024fHeniker4 years ago | 4 | import * as nls from "vscode-nls"; |
| 5 | import { QuickPickOptions, window } from "vscode"; | |
4cd25962JiglioNero4 years ago | 6 | import { ChildProcess } from "../../common/node/childProcess"; |
ab0238b7RedMickey4 years ago | 7 | import { PromiseUtil } from "../../common/node/promise"; |
176f99c8ConnorQi013 months ago | 8 | import { IDebuggableMobileTarget, IMobileTarget, MobileTarget } from "../mobileTarget"; |
4cd25962JiglioNero4 years ago | 9 | import { MobileTargetManager } from "../mobileTargetManager"; |
| 10 | import { OutputChannelLogger } from "../log/OutputChannelLogger"; | |
| 11 | import { TargetType } from "../generalPlatform"; | |
09f6024fHeniker4 years ago | 12 | |
4cd25962JiglioNero4 years ago | 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; | |
176f99c8ConnorQi013 months ago | 26 | protected _name!: string; |
4cd25962JiglioNero4 years ago | 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 | ); | |
09f6024fHeniker4 years ago | 101 | // Output example: |
4cd25962JiglioNero4 years ago | 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) | |
09f6024fHeniker4 years ago | 109 | // ... |
4cd25962JiglioNero4 years ago | 110 | const lines = allDevicesOutput |
| 111 | .split("\n") | |
| 112 | .map(line => line.trim()) | |
| 113 | .filter(line => !!line); | |
09f6024fHeniker4 years ago | 114 | const firstDevicesIndex = lines.indexOf("== Devices ==") + 1; |
| 115 | const lastDevicesIndex = lines.indexOf("== Simulators ==") - 1; | |
4cd25962JiglioNero4 years ago | 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) { | |
09f6024fHeniker4 years ago | 144 | return !selectedTarget.isOnline && selectedTarget.isVirtualTarget |
| 145 | ? this.launchSimulator(selectedTarget) | |
| 146 | : IOSTarget.fromInterface(selectedTarget); | |
4cd25962JiglioNero4 years ago | 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 | } | |
09f6024fHeniker4 years ago | 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"); | |
4cd25962JiglioNero4 years ago | 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( | |
176f99c8ConnorQi013 months ago | 184 | (el: IMobileTarget) => |
| 185 | (filter ? filter(el as IDebuggableIOSTarget) : true) && | |
| 186 | (system === IOSTargetManager.ANY_SYSTEM | |
| 187 | ? true | |
| 188 | : (el as IDebuggableIOSTarget).system === system), | |
4cd25962JiglioNero4 years ago | 189 | )) as IDebuggableIOSTarget | undefined; |
| 190 | } | |
| 191 | return; | |
| 192 | } | |
| 193 | | |
| 194 | protected async selectSystem( | |
| 195 | filter?: (el: IDebuggableIOSTarget) => boolean, | |
| 196 | ): Promise<string | undefined> { | |
176f99c8ConnorQi013 months ago | 197 | const targets = (await this.getTargetList( |
| 198 | filter as ((el: IMobileTarget) => boolean) | undefined, | |
| 199 | )) as IDebuggableIOSTarget[]; | |
4cd25962JiglioNero4 years ago | 200 | // If we select only from devices, we should not select system |
| 201 | if (!targets.find(target => target.isVirtualTarget)) { | |
| 202 | return IOSTargetManager.ANY_SYSTEM; | |
| 203 | } | |
| 204 | const names: Set<string> = new Set(targets.map(target => target.system)); | |
| 205 | const systemsList = Array.from(names); | |
| 206 | let result: string | undefined = systemsList[0]; | |
| 207 | if (systemsList.length > 1) { | |
| 208 | const quickPickOptions: QuickPickOptions = { | |
| 209 | ignoreFocusOut: true, | |
| 210 | canPickMany: false, | |
| 211 | placeHolder: localize( | |
| 212 | "SelectIOSSystemVersion", | |
| 213 | "Select system version of iOS target", | |
| 214 | ), | |
| 215 | }; | |
| 216 | result = await window.showQuickPick(systemsList, quickPickOptions); | |
| 217 | } | |
| 218 | return result?.toString(); | |
| 219 | } | |
| 220 | | |
| 221 | protected async launchSimulator( | |
| 222 | virtualTarget: IDebuggableIOSTarget, | |
| 223 | ): Promise<IOSTarget | undefined> { | |
| 224 | return new Promise<IOSTarget | undefined>((resolve, reject) => { | |
8df9830bSamriel4 years ago | 225 | let emulatorLaunchFailed = false; |
4cd25962JiglioNero4 years ago | 226 | const emulatorProcess = this.childProcess.spawn( |
| 227 | IOSTargetManager.XCRUN_COMMAND, | |
| 228 | [IOSTargetManager.SIMCTL_COMMAND, IOSTargetManager.BOOT_COMMAND, virtualTarget.id], | |
| 229 | { | |
| 230 | detached: true, | |
| 231 | }, | |
| 232 | true, | |
| 233 | ); | |
| 234 | emulatorProcess.spawnedProcess.unref(); | |
| 235 | emulatorProcess.outcome.catch(e => { | |
8df9830bSamriel4 years ago | 236 | emulatorLaunchFailed = true; |
4cd25962JiglioNero4 years ago | 237 | this.logger.error( |
| 238 | localize( | |
| 239 | "ErrorWhileLaunchingSimulator", | |
| 240 | "Error while launching simulator {0} : {1}", | |
| 241 | `${virtualTarget.name}(${virtualTarget.id})`, | |
| 242 | e, | |
| 243 | ), | |
| 244 | ); | |
| 245 | reject(e); | |
| 246 | }); | |
| 247 | | |
| 248 | const condition = async () => { | |
8df9830bSamriel4 years ago | 249 | if (emulatorLaunchFailed) |
| 250 | throw new Error("iOS simulator launch failed unexpectedly"); | |
4cd25962JiglioNero4 years ago | 251 | await this.collectTargets(TargetType.Simulator); |
| 252 | const onlineTarget = (await this.getTargetList()).find( | |
| 253 | target => target.id === virtualTarget.id && target.isOnline, | |
| 254 | ); | |
| 255 | return onlineTarget ? true : null; | |
| 256 | }; | |
| 257 | | |
09f6024fHeniker4 years ago | 258 | void PromiseUtil.waitUntil<boolean>( |
4cd25962JiglioNero4 years ago | 259 | condition, |
| 260 | 1000, | |
| 261 | IOSTargetManager.SIMULATOR_START_TIMEOUT * 1000, | |
8df9830bSamriel4 years ago | 262 | ).then( |
| 263 | isBooted => { | |
| 264 | if (isBooted) { | |
| 265 | virtualTarget.isOnline = true; | |
| 266 | this.logger.info( | |
| 267 | localize( | |
| 268 | "SimulatorLaunched", | |
| 269 | "Launched simulator {0}", | |
4cd25962JiglioNero4 years ago | 270 | virtualTarget.name, |
8df9830bSamriel4 years ago | 271 | ), |
| 272 | ); | |
| 273 | resolve(IOSTarget.fromInterface(virtualTarget)); | |
| 274 | } else { | |
| 275 | reject( | |
| 276 | new Error( | |
| 277 | `Virtual device launch finished with an exception: ${localize( | |
| 278 | "SimulatorStartWarning", | |
| 279 | "Could not start the simulator {0} within {1} seconds.", | |
| 280 | virtualTarget.name, | |
| 281 | IOSTargetManager.SIMULATOR_START_TIMEOUT, | |
| 282 | )}`, | |
| 283 | ), | |
| 284 | ); | |
| 285 | } | |
| 286 | }, | |
| 287 | () => {}, | |
| 288 | ); | |
4cd25962JiglioNero4 years ago | 289 | }); |
| 290 | } | |
| 291 | } |