// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import * as vscode from "vscode"; import { logger } from "@vscode/debugadapter"; import { DebugProtocol } from "vscode-debugprotocol"; import * as nls from "vscode-nls"; import { ProjectVersionHelper } from "../../common/projectVersionHelper"; import { TelemetryHelper } from "../../common/telemetryHelper"; import { HermesCDPMessageHandler } from "../../cdp-proxy/CDPMessageHandlers/hermesCDPMessageHandler"; import { DebugSessionBase, DebugSessionStatus, IAttachRequestArgs, ILaunchRequestArgs, } from "../debugSessionBase"; import { JsDebugConfigAdapter } from "../jsDebugConfigAdapter"; import { DebuggerEndpointHelper } from "../../cdp-proxy/debuggerEndpointHelper"; import { ErrorHelper } from "../../common/error/errorHelper"; import { InternalErrorCode } from "../../common/error/internalErrorCode"; import { IOSDirectCDPMessageHandler } from "../../cdp-proxy/CDPMessageHandlers/iOSDirectCDPMessageHandler"; import { PlatformType } from "../../extension/launchArgs"; import { BaseCDPMessageHandler } from "../../cdp-proxy/CDPMessageHandlers/baseCDPMessageHandler"; import { TipNotificationService } from "../../extension/services/tipsNotificationsService/tipsNotificationService"; import { RNSession } from "../debugSessionWrapper"; import { SettingsHelper } from "../../extension/settingsHelper"; import { ReactNativeProjectHelper } from "../../common/reactNativeProjectHelper"; import { IWDPHelper } from "./IWDPHelper"; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone, })(); const localize = nls.loadMessageBundle(); export class DirectDebugSession extends DebugSessionBase { private debuggerEndpointHelper: DebuggerEndpointHelper; private onDidTerminateDebugSessionHandler: vscode.Disposable; private onDidStartDebugSessionHandler: vscode.Disposable; private appTargetConnectionClosedHandlerDescriptor?: vscode.Disposable; private attachSession: vscode.DebugSession | null; private iOSWKDebugProxyHelper: IWDPHelper; constructor(rnSession: RNSession) { super(rnSession); this.debuggerEndpointHelper = new DebuggerEndpointHelper(); this.iOSWKDebugProxyHelper = new IWDPHelper(); this.attachSession = null; this.onDidTerminateDebugSessionHandler = vscode.debug.onDidTerminateDebugSession( this.handleTerminateDebugSession.bind(this), ); this.onDidStartDebugSessionHandler = vscode.debug.onDidStartDebugSession( this.handleStartDebugSession.bind(this), ); } protected async launchRequest( response: DebugProtocol.LaunchResponse, launchArgs: ILaunchRequestArgs, // eslint-disable-next-line @typescript-eslint/no-unused-vars request?: DebugProtocol.Request, ): Promise { let extProps = { platform: { value: launchArgs.platform, isPii: false, }, isDirect: { value: true, isPii: false, }, }; void TipNotificationService.getInstance().setKnownDateForFeatureById( "directDebuggingWithHermes", ); try { try { if (launchArgs.platform != "exponent") { await ReactNativeProjectHelper.verifyMetroConfigFile(launchArgs.cwd); } await this.initializeSettings(launchArgs); logger.log("Launching the application"); logger.verbose(`Launching the application: ${JSON.stringify(launchArgs, null, 2)}`); const versions = await ProjectVersionHelper.getReactNativeVersions( this.projectRootPath, ProjectVersionHelper.generateAdditionalPackagesToCheckByPlatform(launchArgs), ); extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties( launchArgs, versions, extProps, ); // eslint-disable-next-line @typescript-eslint/no-unused-vars await TelemetryHelper.generate("launch", extProps, generator => this.appLauncher.launch(launchArgs), ); if (!launchArgs.enableDebug) { this.sendResponse(response); // if debugging is not enabled skip attach request return; } } catch (error) { throw ErrorHelper.getInternalError( InternalErrorCode.ApplicationLaunchFailed, (error as Error).message || error, ); } // if debugging is enabled start attach request await this.vsCodeDebugSession.customRequest("attach", launchArgs); this.sendResponse(response); } catch (error) { this.terminateWithErrorResponse(error as Error, response); } } protected async attachRequest( response: DebugProtocol.AttachResponse, attachArgs: IAttachRequestArgs, // eslint-disable-next-line @typescript-eslint/no-unused-vars request?: DebugProtocol.Request, ): Promise { let extProps = { platform: { value: attachArgs.platform, isPii: false, }, isDirect: { value: true, isPii: false, }, }; attachArgs.webkitRangeMin = attachArgs.webkitRangeMin || 9223; attachArgs.webkitRangeMax = attachArgs.webkitRangeMax || 9322; this.previousAttachArgs = attachArgs; try { await this.initializeSettings(attachArgs); const packager = this.appLauncher.getPackager(); const args: Parameters = [ // message indicates that another debugger has connected "Already connected:", { type: "client_log", level: "warn", mode: "BRIDGE", }, ]; void packager.forMessage(...args).then( () => { this.showError( ErrorHelper.getInternalError( InternalErrorCode.AnotherDebuggerConnectedToPackager, ), ); void this.terminate(); }, () => {}, ); logger.log("Attaching to the application"); logger.verbose(`Attaching to the application: ${JSON.stringify(attachArgs, null, 2)}`); const versions = await ProjectVersionHelper.getReactNativeVersions( this.projectRootPath, ProjectVersionHelper.generateAdditionalPackagesToCheckByPlatform(attachArgs), ); extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties( attachArgs, versions, extProps, ); // eslint-disable-next-line @typescript-eslint/no-unused-vars await TelemetryHelper.generate("attach", extProps, async generator => { const port = attachArgs.useHermesEngine ? attachArgs.port || this.appLauncher.getPackagerPort(attachArgs.cwd) : attachArgs.platform === PlatformType.iOS ? attachArgs.port || IWDPHelper.iOS_WEBKIT_DEBUG_PROXY_DEFAULT_PORT : null; if (port === null) { throw ErrorHelper.getInternalError( InternalErrorCode.CouldNotDirectDebugWithoutHermesEngine, attachArgs.platform, ); } attachArgs.port = port; logger.log(`Connecting to ${attachArgs.port} port`); await this.appLauncher.getRnCdpProxy().stopServer(); const cdpMessageHandler: BaseCDPMessageHandler | null = attachArgs.useHermesEngine ? new HermesCDPMessageHandler() : attachArgs.platform === PlatformType.iOS ? new IOSDirectCDPMessageHandler() : null; if (!cdpMessageHandler) { throw ErrorHelper.getInternalError( InternalErrorCode.CouldNotDirectDebugWithoutHermesEngine, attachArgs.platform, ); } await this.appLauncher .getRnCdpProxy() .initializeServer( cdpMessageHandler, this.cdpProxyLogLevel, this.cancellationTokenSource.token, ); if (!attachArgs.useHermesEngine && attachArgs.platform === PlatformType.iOS) { await this.iOSWKDebugProxyHelper.startiOSWebkitDebugProxy( attachArgs.port, attachArgs.webkitRangeMin, attachArgs.webkitRangeMax, ); const results = await this.iOSWKDebugProxyHelper.getSimulatorProxyPort( attachArgs, ); attachArgs.port = results.targetPort; } if (attachArgs.request === "attach") { await this.preparePackagerBeforeAttach(attachArgs, versions); } this.appTargetConnectionClosedHandlerDescriptor = this.appLauncher .getRnCdpProxy() .onApplicationTargetConnectionClosed(() => { if (this.attachSession) { if ( this.debugSessionStatus !== DebugSessionStatus.Stopping && this.debugSessionStatus !== DebugSessionStatus.Stopped ) { void this.terminate(); } this.appTargetConnectionClosedHandlerDescriptor?.dispose(); } }); const settingsPorts = SettingsHelper.getPackagerPort(attachArgs.cwd); const browserInspectUri = await this.debuggerEndpointHelper.retryGetWSEndpoint( `http://localhost:${attachArgs.port}`, 90, this.cancellationTokenSource.token, attachArgs.useHermesEngine, settingsPorts, ); // Make sure expo app is using correct ws endpoint url const debuggerType = await this.debuggerEndpointHelper.getDebuggerTpye( `http://localhost:${attachArgs.port}`, ); if (debuggerType == "expo") { const expoBrowserInspectUri = `${browserInspectUri.split("&")[0]}&page=2`; this.appLauncher.getRnCdpProxy().setBrowserInspectUri(expoBrowserInspectUri); } else { this.appLauncher.getRnCdpProxy().setBrowserInspectUri(browserInspectUri); } await this.establishDebugSession(attachArgs); }); this.sendResponse(response); } catch (error) { this.terminateWithErrorResponse( ErrorHelper.getInternalError( InternalErrorCode.CouldNotAttachToDebugger, (error as Error).message || error, ), response, ); } } protected async disconnectRequest( response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request, ): Promise { this.debugSessionStatus = DebugSessionStatus.Stopping; // Stop packager when using expo-cli, to avoid launch conflicts in the next launch request if (this.appLauncher.getPackager().getPlatform() == "exponent") { await this.appLauncher.getPackager().stop(); } this.iOSWKDebugProxyHelper.cleanUp(); this.onDidTerminateDebugSessionHandler.dispose(); this.onDidStartDebugSessionHandler.dispose(); this.appLauncher.getPackager().closeWsConnection(); this.appTargetConnectionClosedHandlerDescriptor?.dispose(); return super.disconnectRequest(response, args, request); } protected async establishDebugSession(attachArgs: IAttachRequestArgs): Promise { const attachConfiguration = await JsDebugConfigAdapter.createDebuggingConfigForRNHermes( attachArgs, this.appLauncher.getCdpProxyPort(), this.rnSession.sessionId, ); const childDebugSessionStarted = await vscode.debug.startDebugging( this.appLauncher.getWorkspaceFolder(), attachConfiguration, { parentSession: this.vsCodeDebugSession, consoleMode: vscode.DebugConsoleMode.MergeWithParent, }, ); if (!childDebugSessionStarted) { throw new Error( localize("CouldNotStartChildDebugSession", "Couldn't start child debug session"), ); } } private handleTerminateDebugSession(debugSession: vscode.DebugSession): void { if ( debugSession.configuration.rnDebugSessionId === this.rnSession.sessionId && debugSession.type === this.pwaNodeSessionName ) { void this.terminate(); } } private handleStartDebugSession(debugSession: vscode.DebugSession): void { if ( this.nodeSession && (debugSession as any).parentSession && this.nodeSession.id === (debugSession as any).parentSession.id ) { this.attachSession = debugSession; } if ( debugSession.configuration.rnDebugSessionId === this.rnSession.sessionId && debugSession.type === this.pwaNodeSessionName ) { this.nodeSession = debugSession; } } protected async initializeSettings(args: any): Promise { await super.initializeSettings(args); if (args.useHermesEngine === undefined) { args.useHermesEngine = true; } } }