microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
dev/v-peq/removeNode10TodoComments

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/extension/ios/iOSPlatform.ts

470lines · 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
4import * as path from "path";
5import * as semver from "semver";
6
7import * as nls from "vscode-nls";
8import { ChildProcess } from "../../common/node/childProcess";
9import { CommandExecutor } from "../../common/commandExecutor";
10import { MobilePlatformDeps, TargetType } from "../generalPlatform";
11import { IIOSRunOptions, PlatformType } from "../launchArgs";
12import { OutputVerifier, PatternToFailure } from "../../common/outputVerifier";
13import { TelemetryHelper } from "../../common/telemetryHelper";
14import { InternalErrorCode } from "../../common/error/internalErrorCode";
15import { AppLauncher } from "../appLauncher";
16import { GeneralMobilePlatform } from "../generalMobilePlatform";
17import { ErrorHelper } from "../../common/error/errorHelper";
18import { ProjectVersionHelper } from "../../common/projectVersionHelper";
19import { IDebuggableIOSTarget, IOSTarget, IOSTargetManager } from "./iOSTargetManager";
20import { IOSDebugModeManager } from "./iOSDebugModeManager";
21import { PlistBuddy } from "./plistBuddy";
22
23nls.config({
24 messageFormat: nls.MessageFormat.bundle,
25 bundleFormat: nls.BundleFormat.standalone,
26})();
27const localize = nls.loadMessageBundle();
28
29export class IOSPlatform extends GeneralMobilePlatform {
30 public static DEFAULT_IOS_PROJECT_RELATIVE_PATH = "ios";
31
32 private static readonly NEW_RN_CLI_BEHAVIOUR_VERSION = "0.60.0";
33
34 private plistBuddy = new PlistBuddy();
35 private iosProjectRoot: string;
36 private iosDebugModeManager: IOSDebugModeManager;
37
38 private defaultConfiguration: string = "Debug";
39 private configurationArgumentName: string = "--configuration";
40
41 protected target?: IOSTarget;
42
43 // We should add the common iOS build/run errors we find to this list
44 private static RUN_IOS_FAILURE_PATTERNS: PatternToFailure[] = [
45 {
46 pattern: "No devices are booted",
47 errorCode: InternalErrorCode.IOSSimulatorNotLaunchable,
48 },
49 {
50 pattern: "FBSOpenApplicationErrorDomain",
51 errorCode: InternalErrorCode.IOSSimulatorNotLaunchable,
52 },
53 {
54 pattern: "ios-deploy",
55 errorCode: InternalErrorCode.IOSDeployNotFound,
56 },
57 ];
58
59 private static readonly RUN_IOS_SUCCESS_PATTERNS = [
60 "BUILD SUCCEEDED|success Successfully built the app",
61 ];
62
63 constructor(protected runOptions: IIOSRunOptions, platformDeps: MobilePlatformDeps = {}) {
64 super(runOptions, platformDeps);
65
66 this.targetManager = new IOSTargetManager();
67 this.runOptions.configuration = this.getConfiguration();
68
69 if (this.runOptions.iosRelativeProjectPath) {
70 // Deprecated option
71 this.logger.warning(
72 localize(
73 "iosRelativeProjectPathOptionIsDeprecatedUseRunArgumentsInstead",
74 "'iosRelativeProjectPath' option is deprecated. Please use 'runArguments' instead.",
75 ),
76 );
77 }
78
79 const iosProjectFolderPath = IOSPlatform.getOptFromRunArgs(
80 this.runArguments,
81 "--project-path",
82 false,
83 );
84 this.iosProjectRoot = path.join(
85 this.projectPath,
86 iosProjectFolderPath ||
87 this.runOptions.iosRelativeProjectPath ||
88 IOSPlatform.DEFAULT_IOS_PROJECT_RELATIVE_PATH,
89 );
90 const schemeFromArgs = IOSPlatform.getOptFromRunArgs(this.runArguments, "--scheme", false);
91 this.iosDebugModeManager = new IOSDebugModeManager(
92 this.iosProjectRoot,
93 this.projectPath,
94 schemeFromArgs ? schemeFromArgs : this.runOptions.scheme,
95 );
96 }
97
98 public async getTarget(): Promise<IOSTarget> {
99 if (!this.target) {
100 const targetFromRunArgs = await this.getTargetFromRunArgs();
101 if (targetFromRunArgs) {
102 this.target = targetFromRunArgs;
103 } else {
104 const targets =
105 (await this.targetManager.getTargetList()) as IDebuggableIOSTarget[];
106 const targetsBySpecifiedType = targets.filter(target => {
107 switch (this.runOptions.target) {
108 case TargetType.Simulator:
109 return target.isVirtualTarget;
110 case TargetType.Device:
111 return !target.isVirtualTarget;
112 case undefined:
113 case "":
114 return true;
115 default:
116 return (
117 target.id === this.runOptions.target ||
118 target.name === this.runOptions.target
119 );
120 }
121 });
122 if (targetsBySpecifiedType.length) {
123 this.target = IOSTarget.fromInterface(targetsBySpecifiedType[0]);
124 } else if (targets.length) {
125 this.logger.warning(
126 localize(
127 "ThereIsNoTargetWithSpecifiedTargetType",
128 "There is no any target with specified target type '{0}'. Continue with any target.",
129 this.runOptions.target,
130 ),
131 );
132 this.target = IOSTarget.fromInterface(targets[0]);
133 } else {
134 throw ErrorHelper.getInternalError(
135 InternalErrorCode.IOSThereIsNoAnyDebuggableTarget,
136 );
137 }
138 }
139 }
140 return this.target;
141 }
142
143 public async showDevMenu(appLauncher: AppLauncher): Promise<void> {
144 const worker = appLauncher.getAppWorker();
145 if (worker) {
146 worker.showDevMenuCommand();
147 }
148 }
149
150 public async reloadApp(appLauncher: AppLauncher): Promise<void> {
151 const worker = appLauncher.getAppWorker();
152 if (worker) {
153 worker.reloadAppCommand();
154 }
155 }
156
157 public async runApp(): Promise<void> {
158 let extProps: any = {
159 platform: {
160 value: PlatformType.iOS,
161 isPii: false,
162 },
163 };
164
165 if (this.runOptions.isDirect) {
166 extProps.isDirect = {
167 value: true,
168 isPii: false,
169 };
170 this.projectObserver?.updateRNIosHermesProjectState(true);
171 }
172
173 extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties(
174 this.runOptions,
175 this.runOptions.reactNativeVersions,
176 extProps,
177 );
178
179 await TelemetryHelper.generate("iOSPlatform.runApp", extProps, async () => {
180 // Compile, deploy, and launch the app on either a simulator or a device
181 const env = GeneralMobilePlatform.getEnvArgument(
182 process.env,
183 this.runOptions.env,
184 this.runOptions.envFile,
185 );
186
187 if (
188 !semver.valid(
189 this.runOptions.reactNativeVersions.reactNativeVersion,
190 ) /* Custom RN implementations should support this flag*/ ||
191 semver.gte(
192 this.runOptions.reactNativeVersions.reactNativeVersion,
193 IOSPlatform.NO_PACKAGER_VERSION,
194 ) ||
195 ProjectVersionHelper.isCanaryVersion(
196 this.runOptions.reactNativeVersions.reactNativeVersion,
197 )
198 ) {
199 this.runArguments.push("--no-packager");
200 }
201 // Since @react-native-community/cli@2.1.0 build output are hidden by default
202 // we are using `--verbose` to show it as it contains `BUILD SUCCESSFUL` and other patterns
203 if (
204 semver.gte(
205 this.runOptions.reactNativeVersions.reactNativeVersion,
206 IOSPlatform.NEW_RN_CLI_BEHAVIOUR_VERSION,
207 ) ||
208 ProjectVersionHelper.isCanaryVersion(
209 this.runOptions.reactNativeVersions.reactNativeVersion,
210 )
211 ) {
212 this.runArguments.push("--verbose");
213 }
214 const runIosSpawn = new CommandExecutor(
215 this.runOptions.nodeModulesRoot,
216 this.projectPath,
217 this.logger,
218 ).spawnReactCommand("run-ios", this.runArguments, { env });
219 await new OutputVerifier(
220 () =>
221 this.generateSuccessPatterns(
222 this.runOptions.reactNativeVersions.reactNativeVersion,
223 ),
224 () => Promise.resolve(IOSPlatform.RUN_IOS_FAILURE_PATTERNS),
225 PlatformType.iOS,
226 ).process(runIosSpawn);
227
228 // Save target info for status indicator
229 if (!this.target) {
230 const target = await this.getTarget();
231 if (target && target.name) {
232 this.target = new IOSTarget(
233 target.isOnline,
234 target.isVirtualTarget,
235 target.id,
236 target.name,
237 target.system,
238 );
239 }
240 }
241 });
242 }
243
244 public async enableJSDebuggingMode(): Promise<void> {
245 // Configure the app for debugging
246 if (!(await this.getTarget()).isVirtualTarget) {
247 // Note that currently we cannot automatically switch the device into debug mode.
248 this.logger.info(
249 "Application is running on a device, please shake device and select 'Debug JS Remotely' to enable debugging.",
250 );
251 return;
252 }
253
254 // Wait until the configuration file exists, and check to see if debugging is enabled
255 const [debugModeEnabled, bundleId] = await Promise.all<boolean | string>([
256 this.iosDebugModeManager.getAppRemoteDebuggingSetting(
257 this.runOptions.configuration,
258 this.runOptions.productName,
259 ),
260 this.getBundleId(),
261 ]);
262 if (debugModeEnabled) {
263 return;
264 }
265 // Debugging must still be enabled
266 // We enable debugging by writing to a plist file that backs a NSUserDefaults object,
267 // but that file is written to by the app on occasion. To avoid races, we shut the app
268 // down before writing to the file.
269 const childProcess = new ChildProcess();
270 const output = await childProcess.execToString("xcrun simctl spawn booted launchctl list");
271 // Try to find an entry that looks like UIKitApplication:com.example.myApp[0x4f37]
272 const regex = new RegExp(`(\\S+${String(bundleId)}\\S+)`);
273 const match = regex.exec(output);
274 // If we don't find a match, the app must not be running and so we do not need to close it
275 if (match) {
276 await childProcess.exec(`xcrun simctl spawn booted launchctl stop ${match[1]}`);
277 }
278 // Write to the settings file while the app is not running to avoid races
279 await this.iosDebugModeManager.setAppRemoteDebuggingSetting(
280 /* enable=*/ true,
281 this.runOptions.configuration,
282 this.runOptions.productName,
283 );
284 // Relaunch the app
285 return await this.runApp();
286 }
287
288 public async disableJSDebuggingMode(): Promise<void> {
289 if (!(await this.getTarget()).isVirtualTarget) {
290 return;
291 }
292 return this.iosDebugModeManager.setAppRemoteDebuggingSetting(
293 /* enable=*/ false,
294 this.runOptions.configuration,
295 this.runOptions.productName,
296 );
297 }
298
299 public prewarmBundleCache(): Promise<void> {
300 return this.packager.prewarmBundleCache(PlatformType.iOS);
301 }
302
303 public getRunArguments(): string[] {
304 let runArguments: string[] = [];
305
306 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
307 runArguments = this.runOptions.runArguments;
308 if (this.runOptions.scheme) {
309 const schemeFromArgs = IOSPlatform.getOptFromRunArgs(
310 runArguments,
311 "--scheme",
312 false,
313 );
314 if (!schemeFromArgs) {
315 runArguments.push("--scheme", this.runOptions.scheme);
316 } else {
317 this.logger.warning(
318 localize(
319 "iosSchemeParameterAlreadySetInRunArguments",
320 "'--scheme' is set as 'runArguments' configuration parameter value, 'scheme' configuration parameter value will be omitted",
321 ),
322 );
323 }
324 }
325 } else {
326 if (this.runOptions.target) {
327 runArguments.push(...this.handleTargetArg(this.runOptions.target));
328 }
329
330 if (this.runOptions.iosRelativeProjectPath) {
331 runArguments.push("--project-path", this.runOptions.iosRelativeProjectPath);
332 }
333
334 // provide any defined scheme
335 if (this.runOptions.scheme) {
336 runArguments.push("--scheme", this.runOptions.scheme);
337 }
338 }
339
340 return runArguments;
341 }
342
343 public async getTargetFromRunArgs(): Promise<IOSTarget | undefined> {
344 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
345 const targets = (await this.targetManager.getTargetList()) as IDebuggableIOSTarget[];
346
347 const udid = GeneralMobilePlatform.getOptFromRunArgs(
348 this.runOptions.runArguments,
349 "--udid",
350 );
351 if (udid) {
352 const target = targets.find(target => target.id === udid);
353 if (target) {
354 return IOSTarget.fromInterface(target);
355 }
356 this.logger.warning(
357 localize(
358 "ThereIsNoIosTargetWithSuchUdid",
359 "There is no iOS target with such UDID: {0}",
360 udid,
361 ),
362 );
363 }
364
365 const device = GeneralMobilePlatform.getOptFromRunArgs(
366 this.runOptions.runArguments,
367 "--device",
368 );
369 if (device) {
370 const target = targets.find(
371 target => !target.isVirtualTarget && target.name === device,
372 );
373 if (target) {
374 return IOSTarget.fromInterface(target);
375 }
376 this.logger.warning(
377 localize(
378 "ThereIsNoIosDeviceWithSuchName",
379 "There is no iOS device with such name: {0}",
380 device,
381 ),
382 );
383 }
384
385 const simulator = GeneralMobilePlatform.getOptFromRunArgs(
386 this.runOptions.runArguments,
387 "--simulator",
388 );
389 if (simulator) {
390 const target = targets.find(
391 target => target.isVirtualTarget && target.name === simulator,
392 );
393 if (target) {
394 return IOSTarget.fromInterface(target);
395 }
396 this.logger.warning(
397 localize(
398 "ThereIsNoIosSimulatorWithSuchName",
399 "There is no iOS simulator with such name: {0}",
400 simulator,
401 ),
402 );
403 }
404 }
405
406 return undefined;
407 }
408
409 private handleTargetArg(target: string): string[] {
410 return target === TargetType.Device || target === TargetType.Simulator
411 ? [`--${target}`]
412 : ["--udid", target];
413 }
414
415 private async generateSuccessPatterns(version: string): Promise<string[]> {
416 // Clone RUN_IOS_SUCCESS_PATTERNS to avoid its runtime mutation
417 const successPatterns = [...IOSPlatform.RUN_IOS_SUCCESS_PATTERNS];
418 if (!(await this.getTarget()).isVirtualTarget) {
419 if (
420 semver.gte(version, IOSPlatform.NEW_RN_CLI_BEHAVIOUR_VERSION) ||
421 ProjectVersionHelper.isCanaryVersion(version)
422 ) {
423 successPatterns.push("success Installed the app on the device");
424 } else {
425 successPatterns.push("INSTALLATION SUCCEEDED");
426 }
427 return successPatterns;
428 }
429 const bundleId = await this.getBundleId();
430 if (
431 semver.gte(version, IOSPlatform.NEW_RN_CLI_BEHAVIOUR_VERSION) ||
432 ProjectVersionHelper.isCanaryVersion(version)
433 ) {
434 successPatterns.push(`Launching "${bundleId}"\nsuccess Successfully launched the app`);
435 } else {
436 successPatterns.push(`Launching ${bundleId}\n${bundleId}: `);
437 }
438 return successPatterns;
439 }
440
441 private getConfiguration(): string {
442 return (
443 IOSPlatform.getOptFromRunArgs(this.runArguments, this.configurationArgumentName) ||
444 this.defaultConfiguration
445 );
446 }
447
448 private getBundleId(): Promise<string> {
449 let scheme = this.runOptions.scheme;
450 if (!scheme) {
451 const schemeFromArgs = IOSPlatform.getOptFromRunArgs(
452 this.runArguments,
453 "--scheme",
454 false,
455 );
456 if (schemeFromArgs) {
457 scheme = schemeFromArgs;
458 }
459 }
460 return this.plistBuddy.getBundleId(
461 this.iosProjectRoot,
462 this.projectPath,
463 PlatformType.iOS,
464 true,
465 this.runOptions.configuration,
466 this.runOptions.productName,
467 scheme,
468 );
469 }
470}
471