microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8f322bcd71555e02f0b00a89a99da351ef8412b7

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/ios/iOSPlatform.ts

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