// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import * as path from "path"; import * as vscode from "vscode"; import * as mkdirp from "mkdirp"; import { logger } from "@vscode/debugadapter"; import { DebugProtocol } from "vscode-debugprotocol"; import * as nls from "vscode-nls"; import * as semver from "semver"; import { ProjectVersionHelper } from "../common/projectVersionHelper"; import { InternalErrorCode } from "../common/error/internalErrorCode"; import { TelemetryHelper } from "../common/telemetryHelper"; import { RnCDPMessageHandler } from "../cdp-proxy/CDPMessageHandlers/rnCDPMessageHandler"; import { ErrorHelper } from "../common/error/errorHelper"; import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper"; import { MultipleLifetimesAppWorker } from "./appWorker"; import { DebugSessionBase, DebugSessionStatus, IAttachRequestArgs, ILaunchRequestArgs, } from "./debugSessionBase"; import { JsDebugConfigAdapter } from "./jsDebugConfigAdapter"; import { RNSession } from "./debugSessionWrapper"; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone, })(); const localize = nls.loadMessageBundle(); export class RNDebugSession extends DebugSessionBase { private appWorker: MultipleLifetimesAppWorker | null; private onDidStartDebugSessionHandler: vscode.Disposable; private onDidTerminateDebugSessionHandler: vscode.Disposable; constructor(rnSession: RNSession) { super(rnSession); // variables definition this.appWorker = null; this.onDidStartDebugSessionHandler = vscode.debug.onDidStartDebugSession( this.handleStartDebugSession.bind(this), ); this.onDidTerminateDebugSessionHandler = vscode.debug.onDidTerminateDebugSession( this.handleTerminateDebugSession.bind(this), ); } protected async launchRequest( response: DebugProtocol.LaunchResponse, launchArgs: ILaunchRequestArgs, // eslint-disable-next-line @typescript-eslint/no-unused-vars request?: DebugProtocol.Request, ): Promise { try { try { if (launchArgs.platform != "exponent") { await ReactNativeProjectHelper.verifyMetroConfigFile(launchArgs.cwd); } const requiredNodeRange = await ProjectVersionHelper.getReactNativeRequiredNodeRange(launchArgs.cwd); if (requiredNodeRange) { const currentNode = process.version; const coerced = semver.coerce(currentNode); if ( !coerced || !semver.satisfies(coerced, requiredNodeRange, { includePrerelease: true }) ) { throw ErrorHelper.getInternalError( InternalErrorCode.CouldNotAttachToDebugger, `Incompatible Node.js for React Native: detected ${currentNode}; required ${requiredNodeRange}. Install a supported Node.js or change React Native version.`, ); } } await this.initializeSettings(launchArgs); logger.log("Launching the application"); logger.verbose(`Launching the application: ${JSON.stringify(launchArgs, null, 2)}`); await 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, }, }; this.previousAttachArgs = attachArgs; return new Promise(async (resolve, reject) => { try { const requiredNodeRange = await ProjectVersionHelper.getReactNativeRequiredNodeRange(attachArgs.cwd); if (requiredNodeRange) { const currentNode = process.version; const coerced = semver.coerce(currentNode); if ( !coerced || !semver.satisfies(coerced, requiredNodeRange, { includePrerelease: true }) ) { reject( ErrorHelper.getInternalError( InternalErrorCode.CouldNotAttachToDebugger, `Incompatible Node.js for React Native: detected ${currentNode}; required ${requiredNodeRange}. Install a supported Node.js or change React Native version.`, ), ); return; } } await this.initializeSettings(attachArgs); 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 => { attachArgs.port = attachArgs.port || this.appLauncher.getPackagerPort(attachArgs.cwd); const cdpProxy = this.appLauncher.getRnCdpProxy(); await cdpProxy.stopServer(); await cdpProxy.initializeServer( new RnCDPMessageHandler(), this.cdpProxyLogLevel, this.cancellationTokenSource.token, ); if (attachArgs.request === "attach") { await this.preparePackagerBeforeAttach(attachArgs, versions); } logger.log( localize("StartingDebuggerAppWorker", "Starting debugger app worker."), ); const sourcesStoragePath = path.join(this.projectRootPath, ".vscode", ".react"); // Create folder if not exist to avoid problems if // RN project root is not a ${workspaceFolder} mkdirp.sync(sourcesStoragePath); // If launch is invoked first time, appWorker is undefined, so create it here this.appWorker = new MultipleLifetimesAppWorker( attachArgs, sourcesStoragePath, this.projectRootPath, this.cancellationTokenSource.token, undefined, ); this.appLauncher.setAppWorker(this.appWorker); this.appWorker.on("connected", (port: number) => { if (this.cancellationTokenSource.token.isCancellationRequested) { return this.appWorker?.stop(); } logger.log( localize( "DebuggerWorkerLoadedRuntimeOnPort", "Debugger worker loaded runtime on port {0}", port, ), ); cdpProxy.setApplicationTargetPort(port); if (this.debugSessionStatus === DebugSessionStatus.ConnectionPending) { return; } if (this.debugSessionStatus === DebugSessionStatus.FirstConnection) { this.debugSessionStatus = DebugSessionStatus.FirstConnectionPending; this.establishDebugSession(attachArgs, resolve); } else if ( this.debugSessionStatus === DebugSessionStatus.ConnectionAllowed ) { if (this.nodeSession) { this.debugSessionStatus = DebugSessionStatus.ConnectionPending; void this.nodeSession.customRequest(this.terminateCommand); } } }); if (this.cancellationTokenSource.token.isCancellationRequested) { return this.appWorker.stop(); } return await this.appWorker.start(); }); } catch (error) { reject(error); } }) .then(() => { this.sendResponse(response); }) .catch(err => this.terminateWithErrorResponse( ErrorHelper.getInternalError( InternalErrorCode.CouldNotAttachToDebugger, err.message || err, ), response, ), ); } protected async disconnectRequest( response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request, ): Promise { // The client is about to disconnect so first we need to stop app worker if (this.appWorker) { this.appWorker.stop(); } this.onDidStartDebugSessionHandler.dispose(); this.onDidTerminateDebugSessionHandler.dispose(); return super.disconnectRequest(response, args, request); } protected establishDebugSession( attachArgs: IAttachRequestArgs, resolve?: (value?: void | PromiseLike | undefined) => void, ): void { const attachConfiguration = JsDebugConfigAdapter.createDebuggingConfigForPureRN( attachArgs, this.appLauncher.getCdpProxyPort(), this.rnSession.sessionId, ); vscode.debug .startDebugging(this.appLauncher.getWorkspaceFolder(), attachConfiguration, { parentSession: this.vsCodeDebugSession, consoleMode: vscode.DebugConsoleMode.MergeWithParent, }) .then( (childDebugSessionStarted: boolean) => { if (childDebugSessionStarted) { this.debugSessionStatus = DebugSessionStatus.ConnectionDone; this.setConnectionAllowedIfPossible(); if (resolve) { this.debugSessionStatus = DebugSessionStatus.ConnectionAllowed; resolve(); } } else { this.debugSessionStatus = DebugSessionStatus.ConnectionFailed; this.setConnectionAllowedIfPossible(); this.resetFirstConnectionStatus(); throw new Error("Cannot start child debug session"); } }, err => { this.debugSessionStatus = DebugSessionStatus.ConnectionFailed; this.setConnectionAllowedIfPossible(); this.resetFirstConnectionStatus(); throw err; }, ); } private handleStartDebugSession(debugSession: vscode.DebugSession): void { if ( debugSession.configuration.rnDebugSessionId === this.rnSession.sessionId && debugSession.type === this.pwaNodeSessionName ) { this.nodeSession = debugSession; } } private handleTerminateDebugSession(debugSession: vscode.DebugSession): void { if ( debugSession.configuration.rnDebugSessionId === this.rnSession.sessionId && debugSession.type === this.pwaNodeSessionName ) { if (this.debugSessionStatus === DebugSessionStatus.ConnectionPending) { this.establishDebugSession(this.previousAttachArgs); } else { void this.terminate(); } } } private setConnectionAllowedIfPossible(): void { if ( this.debugSessionStatus === DebugSessionStatus.ConnectionDone || this.debugSessionStatus === DebugSessionStatus.ConnectionFailed ) { this.debugSessionStatus = DebugSessionStatus.ConnectionAllowed; } } private resetFirstConnectionStatus(): void { if (this.debugSessionStatus === DebugSessionStatus.FirstConnectionPending) { this.debugSessionStatus = DebugSessionStatus.FirstConnection; } } }