// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import * as fs from "fs"; import * as path from "path"; import * as child_process from "child_process"; import * as vscode from "vscode"; import * as nls from "vscode-nls"; import * as BrowserHelper from "@vscode/js-debug-browsers"; import execa = require("execa"); import { Packager } from "../common/packager"; import { RNPackageVersions, ProjectVersionHelper } from "../common/projectVersionHelper"; import { CommandExecutor } from "../common/commandExecutor"; import { isNullOrUndefined } from "../common/utils"; import { TelemetryHelper } from "../common/telemetryHelper"; import { ErrorHelper } from "../common/error/errorHelper"; import { InternalErrorCode } from "../common/error/internalErrorCode"; import { InternalError } from "../common/error/internalError"; import { TargetPlatformHelper } from "../common/targetPlatformHelper"; import { getNodeModulesInFolderHierarchy, generateRandomPortNumber, } from "../common/extensionHelper"; import { ReactNativeCDPProxy } from "../cdp-proxy/reactNativeCDPProxy"; import { MultipleLifetimesAppWorker } from "../debugger/appWorker"; import { HostPlatform } from "../common/hostPlatform"; import { ProjectsStorage } from "./projectsStorage"; import { PlatformResolver } from "./platformResolver"; import { GeneralPlatform, MobilePlatformDeps, TargetType } from "./generalPlatform"; import { OutputChannelLogger } from "./log/OutputChannelLogger"; import { PackagerStatusIndicator } from "./packagerStatusIndicator"; import { SettingsHelper } from "./settingsHelper"; import { ReactDirManager } from "./reactDirManager"; import { ExponentHelper } from "./exponent/exponentHelper"; import { BROWSER_TYPES, DEBUG_TYPES } from "./debuggingConfiguration/debugConfigTypesAndConstants"; import { IBaseArgs, PlatformType } from "./launchArgs"; import { LaunchScenariosManager } from "./launchScenariosManager"; import { createAdditionalWorkspaceFolder, onFolderAdded } from "./rn-extension"; import { RNProjectObserver } from "./rnProjectObserver"; import { GeneralMobilePlatform } from "./generalMobilePlatform"; import { DeviceStatusIndicator } from "./deviceStatusIndicator"; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone, })(); const localize = nls.loadMessageBundle(); export class AppLauncher { private readonly cdpProxyPort = generateRandomPortNumber(); /** localhost */ private readonly cdpProxyHostAddress = "127.0.0.1"; public static readonly CHROME_DATA_DIR = "chrome_sandbox_dir"; public static readonly EDGE_DATA_DIR = "edge_sandbox_dir"; private appWorker: MultipleLifetimesAppWorker | null = null; private packager: Packager; private exponentHelper: ExponentHelper; private reactNativeVersions?: RNPackageVersions; private rnCdpProxy: ReactNativeCDPProxy; private logger: OutputChannelLogger = OutputChannelLogger.getMainChannel(); private mobilePlatform!: GeneralPlatform; private launchScenariosManager: LaunchScenariosManager; private debugConfigurationRoot: string; private nodeModulesRoot?: string; private browserProc: child_process.ChildProcess | null = null; private browserStopEventEmitter: vscode.EventEmitter = new vscode.EventEmitter(); public static getAppLauncherByProjectRootPath(projectRootPath: string): AppLauncher { const appLauncher = ProjectsStorage.projectsCache[projectRootPath.toLowerCase()]; if (!appLauncher) { throw new Error( `Could not find AppLauncher by the project root path ${projectRootPath}`, ); } return appLauncher; } public static async getOrCreateAppLauncherByProjectRootPath( projectRootPath: string, ): Promise { let appLauncher = ProjectsStorage.projectsCache[projectRootPath.toLowerCase()]; if (!appLauncher) { const appLauncherFolder = createAdditionalWorkspaceFolder(projectRootPath); if (appLauncherFolder) { await onFolderAdded(appLauncherFolder); appLauncher = ProjectsStorage.projectsCache[appLauncherFolder.uri.fsPath.toLocaleLowerCase()]; } if (!appLauncher) { throw new Error( `Could not find AppLauncher by the project root path ${projectRootPath}`, ); } } return appLauncher; } public static getNodeModulesRootByProjectPath(projectRootPath: string): string { const appLauncher: AppLauncher = AppLauncher.getAppLauncherByProjectRootPath(projectRootPath); return appLauncher.getOrUpdateNodeModulesRoot(); } constructor( private reactDirManager: ReactDirManager, private projectObserver: RNProjectObserver, private workspaceFolder: vscode.WorkspaceFolder, ) { this.debugConfigurationRoot = workspaceFolder.uri.fsPath; const projectRootPath = SettingsHelper.getReactNativeProjectRoot( this.debugConfigurationRoot, ); this.launchScenariosManager = new LaunchScenariosManager(this.debugConfigurationRoot); this.exponentHelper = new ExponentHelper(this.debugConfigurationRoot, projectRootPath); this.packager = new Packager( this.debugConfigurationRoot, projectRootPath, SettingsHelper.getPackagerPort(workspaceFolder.uri.fsPath), new PackagerStatusIndicator(this.debugConfigurationRoot), ); this.packager.setExponentHelper(this.exponentHelper); this.rnCdpProxy = new ReactNativeCDPProxy(this.cdpProxyHostAddress, this.cdpProxyPort); } public updateDebugConfigurationRoot(debugConfigurationRoot: string): void { if (this.debugConfigurationRoot !== debugConfigurationRoot) { this.debugConfigurationRoot = debugConfigurationRoot; this.launchScenariosManager = new LaunchScenariosManager(this.debugConfigurationRoot); } } public getCdpProxyPort(): number { return this.cdpProxyPort; } public getRnCdpProxy(): ReactNativeCDPProxy { return this.rnCdpProxy; } public getPackager(): Packager { return this.packager; } public getWorkspaceFolderUri(): vscode.Uri { return this.workspaceFolder.uri; } public getWorkspaceFolder(): vscode.WorkspaceFolder { return this.workspaceFolder; } public getReactNativeVersions(): RNPackageVersions | undefined { return this.reactNativeVersions; } public getExponentHelper(): ExponentHelper { return this.exponentHelper; } public getReactDirManager(): ReactDirManager { return this.reactDirManager; } public setReactNativeVersions(reactNativeVersions: RNPackageVersions): void { this.reactNativeVersions = reactNativeVersions; } public setAppWorker(appWorker: MultipleLifetimesAppWorker): void { this.appWorker = appWorker; } public getAppWorker(): MultipleLifetimesAppWorker | null { return this.appWorker; } public getMobilePlatform(): GeneralPlatform { return this.mobilePlatform; } public getOrUpdateNodeModulesRoot(forceUpdate = false): string { if (!this.nodeModulesRoot || forceUpdate) { const nodeModulesRootPath: string | null = getNodeModulesInFolderHierarchy( this.packager.getProjectPath(), ); if (!nodeModulesRootPath) { throw ErrorHelper.getInternalError( InternalErrorCode.ReactNativePackageIsNotInstalled, ); } this.nodeModulesRoot = nodeModulesRootPath; } return this.nodeModulesRoot as string; } public dispose(): void { this.packager.getStatusIndicator().dispose(); void this.packager.stop(true); this.mobilePlatform.dispose(); DeviceStatusIndicator.hide(); } public async openFileAtLocation(filename: string, lineNumber: number): Promise { const document = await vscode.workspace.openTextDocument(vscode.Uri.file(filename)); const editor = await vscode.window.showTextDocument(document); const range = editor.document.lineAt(lineNumber - 1).range; editor.selection = new vscode.Selection(range.start, range.end); editor.revealRange(range, vscode.TextEditorRevealType.InCenter); } public getPackagerPort(projectFolder: string): number { return SettingsHelper.getPackagerPort(projectFolder); } public prepareBaseRunOptions(args: any): IBaseArgs { const workspaceFolder: vscode.WorkspaceFolder = ( vscode.workspace.getWorkspaceFolder(vscode.Uri.file(args.cwd || args.program)) ); const baseRunOptions: IBaseArgs = { platform: args.platform, workspaceRoot: workspaceFolder.uri.fsPath, projectRoot: this.getProjectRoot(args), env: args.env, envFile: args.envFile, nodeModulesRoot: this.getOrUpdateNodeModulesRoot(), isDirect: args.type === DEBUG_TYPES.REACT_NATIVE_DIRECT, packagerPort: SettingsHelper.getPackagerPort(args.cwd || args.program), }; return baseRunOptions; } public async launch(launchArgs: any): Promise { const mobilePlatformOptions = this.requestSetup(launchArgs); // We add the parameter if it's defined (adapter crashes otherwise) if (!isNullOrUndefined(launchArgs.logCatArguments)) { mobilePlatformOptions.logCatArguments = [ this.parseLogCatArguments(launchArgs.logCatArguments), ]; } if (!isNullOrUndefined(launchArgs.variant)) { mobilePlatformOptions.variant = launchArgs.variant; } if (!isNullOrUndefined(launchArgs.scheme)) { mobilePlatformOptions.scheme = launchArgs.scheme; } if (!isNullOrUndefined(launchArgs.productName)) { mobilePlatformOptions.productName = launchArgs.productName; } if (!isNullOrUndefined(launchArgs.launchActivity)) { mobilePlatformOptions.debugLaunchActivity = launchArgs.launchActivity; } const platformDeps: MobilePlatformDeps = { packager: this.packager, projectObserver: this.projectObserver, }; this.mobilePlatform = new PlatformResolver().resolveMobilePlatform( launchArgs.platform, mobilePlatformOptions, platformDeps, ); let extProps: any = { platform: { value: launchArgs.platform, isPii: false, }, }; if (mobilePlatformOptions.isDirect) { extProps.isDirect = { value: true, isPii: false, }; } try { const versions = await ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules( mobilePlatformOptions.nodeModulesRoot, ProjectVersionHelper.generateAdditionalPackagesToCheckByPlatform(launchArgs), ); mobilePlatformOptions.reactNativeVersions = versions; extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties( launchArgs, versions, extProps, ); await TelemetryHelper.generate("launch", extProps, async generator => { try { if (this.mobilePlatform instanceof GeneralMobilePlatform) { generator.step("resolveMobileTarget"); if (launchArgs.target) { await this.resolveAndSaveMobileTarget(launchArgs, this.mobilePlatform); } } await this.mobilePlatform.beforeStartPackager(); generator.step("checkPlatformCompatibility"); TargetPlatformHelper.checkTargetPlatformSupport(mobilePlatformOptions.platform); generator.step("startPackager"); await this.mobilePlatform.startPackager(); // We've seen that if we don't prewarm the bundle cache, the app fails on the first attempt to connect to the debugger logic // and the user needs to Reload JS manually. We prewarm it to prevent that issue generator.step("prewarmBundleCache"); this.logger.info( localize( "PrewarmingBundleCache", "Prewarming bundle cache. This may take a while ...", ), ); await this.mobilePlatform.prewarmBundleCache(); generator .step("mobilePlatform.runApp") .add("target", mobilePlatformOptions.target, false); this.logger.info( localize( "BuildingAndRunningApplication", "Building and running application.", ), ); if (launchArgs.platform !== "exponent") { await this.mobilePlatform.runApp(); } // Show device name in status bar after app is launched if (this.mobilePlatform instanceof GeneralMobilePlatform) { const target = this.mobilePlatform.getResolvedTarget(); if (target?.name) { DeviceStatusIndicator.show(target.name); } } if (mobilePlatformOptions.isDirect) { if (launchArgs.useHermesEngine) { generator.step("mobilePlatform.enableHermesDebuggingMode"); if (mobilePlatformOptions.enableDebug) { this.logger.info( localize("PrepareHermesDebugging", "Prepare Hermes debugging"), ); } else { this.logger.info( localize("PrepareHermesLaunch", "Prepare Hermes launch"), ); } } else if (launchArgs.platform === PlatformType.iOS) { generator.step("mobilePlatform.enableIosDirectDebuggingMode"); if (mobilePlatformOptions.enableDebug) { this.logger.info( localize( "PrepareDirectIosDebugging", "Prepare direct iOS debugging (experimental)", ), ); } else { this.logger.info( localize( "PrepareDirectIosLaunch", "Prepare direct iOS launch (experimental)", ), ); } } generator.step("mobilePlatform.disableJSDebuggingMode"); this.logger.info(localize("DisableJSDebugging", "Disable JS Debugging")); await this.mobilePlatform.disableJSDebuggingMode(); } else { generator.step("mobilePlatform.enableJSDebuggingMode"); this.logger.info(localize("EnableJSDebugging", "Enable JS Debugging")); await this.mobilePlatform.enableJSDebuggingMode(); } } catch (error) { if ( !mobilePlatformOptions.enableDebug && launchArgs.platform === PlatformType.iOS && launchArgs.type === DEBUG_TYPES.REACT_NATIVE ) { // If we disable debugging mode for iOS scenarios, we'll we ignore the error and run the 'run-ios' command anyway, // since the error doesn't affects an application launch process return; } generator.addError(error as Error); this.logger.error( error instanceof Error ? error.message : String(error), error instanceof Error ? error : undefined, ); throw error; } }); } catch (error) { if (error instanceof InternalError) { if (error.errorCode === InternalErrorCode.ReactNativePackageIsNotInstalled) { TelemetryHelper.sendErrorEvent( "ReactNativePackageIsNotInstalled", ErrorHelper.getInternalError( InternalErrorCode.ReactNativePackageIsNotInstalled, ), ); } else if (error.errorCode === InternalErrorCode.ReactNativeWindowsIsNotInstalled) { TelemetryHelper.sendErrorEvent( "ReactNativeWindowsPackageIsNotInstalled", ErrorHelper.getInternalError( InternalErrorCode.ReactNativeWindowsIsNotInstalled, ), ); } } this.logger.error( error instanceof Error ? error.message : String(error), error instanceof Error ? error : undefined, ); throw error; } } public async launchExpoWeb(launchArgs: any): Promise { const platformOptions = this.requestSetup(launchArgs); const platformDeps: MobilePlatformDeps = { packager: this.packager, projectObserver: this.projectObserver, }; this.mobilePlatform = new PlatformResolver().resolveMobilePlatform( launchArgs.platform, platformOptions, platformDeps, ); let extProps: any = { platform: { value: launchArgs.platform, isPii: false, }, }; if (platformOptions.isDirect) { extProps.isDirect = { value: true, isPii: false, }; } try { const versions = await ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules( platformOptions.nodeModulesRoot, ProjectVersionHelper.generateAdditionalPackagesToCheckByPlatform(launchArgs), ); platformOptions.reactNativeVersions = versions; extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties( launchArgs, versions, extProps, ); await TelemetryHelper.generate("launch", extProps, async generator => { try { await this.mobilePlatform.beforeStartPackager(); generator.step("checkPlatformCompatibility"); TargetPlatformHelper.checkTargetPlatformSupport(platformOptions.platform); generator.step("startPackager"); await this.mobilePlatform.startPackager(); } catch (error) { generator.addError(error as Error); this.logger.error( error instanceof Error ? error.message : String(error), error instanceof Error ? error : undefined, ); throw error; } }); } catch (error) { if (error instanceof InternalError) { if (error.errorCode === InternalErrorCode.ReactNativePackageIsNotInstalled) { TelemetryHelper.sendErrorEvent( "ReactNativePackageIsNotInstalled", ErrorHelper.getInternalError( InternalErrorCode.ReactNativePackageIsNotInstalled, ), ); } else if (error.errorCode === InternalErrorCode.ReactNativeWindowsIsNotInstalled) { TelemetryHelper.sendErrorEvent( "ReactNativeWindowsPackageIsNotInstalled", ErrorHelper.getInternalError( InternalErrorCode.ReactNativeWindowsIsNotInstalled, ), ); } } this.logger.error( error instanceof Error ? error.message : String(error), error instanceof Error ? error : undefined, ); throw error; } } public async launchBrowser(launchArgs: any): Promise { const runArguments = this.getRunArguments(launchArgs); // Launch browser let browserFinder: BrowserHelper.IBrowserFinder; if (launchArgs.platform == PlatformType.ExpoWeb) { switch (launchArgs.browserTarget) { case BROWSER_TYPES.Edge: browserFinder = new BrowserHelper.EdgeBrowserFinder( process.env, fs.promises, execa, ); break; case BROWSER_TYPES.Chrome: default: browserFinder = new BrowserHelper.ChromeBrowserFinder( process.env, fs.promises, execa, ); } const browserPath = (await browserFinder.findAll())[0]; if (browserPath) { this.browserProc = child_process.spawn(browserPath.path, runArguments, { detached: true, stdio: ["ignore"], }); this.browserProc.unref(); this.browserProc.on("error", err => { const errMsg = localize("BrowserError", "Browser error: {0}", err.message); this.logger.error(errMsg); this.browserStopEventEmitter.fire(err); }); this.browserProc.once("exit", (code: number) => { const exitMessage = localize( "BrowserExit", "Browser has been closed with exit code: {0}", code, ); this.logger.info(exitMessage); this.browserStopEventEmitter.fire(undefined); }); } } } public getRunArguments(launchArgs: any): string[] { let userDataDir; if (launchArgs.browserTarget == BROWSER_TYPES.Chrome) { userDataDir = path.join(HostPlatform.getSettingsHome(), AppLauncher.CHROME_DATA_DIR); } else if (launchArgs.browserTarget == BROWSER_TYPES.Edge) { userDataDir = path.join(HostPlatform.getSettingsHome(), AppLauncher.EDGE_DATA_DIR); } else { userDataDir = ""; } const args: string[] = [ `--remote-debugging-port=${(launchArgs.port as number) || 9222}`, "--no-first-run", "--no-default-browser-check", `--user-data-dir=${userDataDir}`, ]; if (launchArgs.url) { args.push(launchArgs.url); } return args; } private async resolveAndSaveMobileTarget( launchArgs: any, mobilePlatform: GeneralMobilePlatform, ): Promise { if (launchArgs.target && !(await mobilePlatform.getTargetFromRunArgs())) { const isAnyTarget = launchArgs.target.toLowerCase() === TargetType.Simulator || launchArgs.target.toLowerCase() === TargetType.Device; const resultTarget = await mobilePlatform.resolveMobileTarget(launchArgs.target); // Save the result to config in case there are more than one possible target with this type (simulator/device) if (resultTarget && isAnyTarget) { const targetsCount = await mobilePlatform.getTargetsCountByFilter( target => target.isVirtualTarget === resultTarget.isVirtualTarget, ); if (targetsCount > 1) { this.launchScenariosManager.updateLaunchScenario(launchArgs, { target: launchArgs.platform === PlatformType.Android ? resultTarget.name : resultTarget.id, }); } } } } private requestSetup(args: any): any { const workspaceFolder: vscode.WorkspaceFolder = ( vscode.workspace.getWorkspaceFolder(vscode.Uri.file(args.cwd || args.program)) ); const mobilePlatformOptions: any = Object.assign( { target: args.target, enableDebug: args.enableDebug }, this.prepareBaseRunOptions(args), ); if (args.platform === PlatformType.Exponent) { mobilePlatformOptions.expoHostType = args.expoHostType || "lan"; mobilePlatformOptions.expoPlatformType = args.expoPlatformType || "Android"; mobilePlatformOptions.openExpoQR = typeof args.openExpoQR !== "boolean" ? true : args.openExpoQR; } CommandExecutor.ReactNativeCommand = SettingsHelper.getReactNativeGlobalCommandName( workspaceFolder.uri, ); if (!args.runArguments) { const runArgs = SettingsHelper.getRunArgs( args.platform, args.target || "simulator", workspaceFolder.uri, ); mobilePlatformOptions.runArguments = runArgs; } else { mobilePlatformOptions.runArguments = args.runArguments; } return mobilePlatformOptions; } private getProjectRoot(args: any): string { return SettingsHelper.getReactNativeProjectRoot(args.cwd || args.program); } /** * Parses log cat arguments to a string */ private parseLogCatArguments(userProvidedLogCatArguments: any): string { return Array.isArray(userProvidedLogCatArguments) ? userProvidedLogCatArguments.join(" ") // If it's an array, we join the arguments : userProvidedLogCatArguments; // If not, we leave it as-is } }