// 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 * as path from "path";
import * as mkdirp from "mkdirp";
import { logger } from "vscode-debugadapter";
import { DebugProtocol } from "vscode-debugprotocol";
import { ProjectVersionHelper } from "../common/projectVersionHelper";
import { TelemetryHelper } from "../common/telemetryHelper";
import { MultipleLifetimesAppWorker } from "./appWorker";
import { RnCDPMessageHandler } from "../cdp-proxy/CDPMessageHandlers/rnCDPMessageHandler";
import {
DebugSessionBase,
DebugSessionStatus,
IAttachRequestArgs,
ILaunchRequestArgs,
} from "./debugSessionBase";
import { JsDebugConfigAdapter } from "./jsDebugConfigAdapter";
import { ErrorHelper } from "../common/error/errorHelper";
import { InternalErrorCode } from "../common/error/internalErrorCode";
import * as nls from "vscode-nls";
nls.config({
messageFormat: nls.MessageFormat.bundle,
bundleFormat: nls.BundleFormat.standalone,
})();
const localize = nls.loadMessageBundle();
export class RNDebugSession extends DebugSessionBase {
private readonly terminateCommand: string;
private appWorker: MultipleLifetimesAppWorker | null;
private nodeSession: vscode.DebugSession | null;
private onDidStartDebugSessionHandler: vscode.Disposable;
private onDidTerminateDebugSessionHandler: vscode.Disposable;
constructor(session: vscode.DebugSession) {
super(session);
// constants definition
this.terminateCommand = "terminate"; // the "terminate" command is sent from the client to the debug adapter in order to give the debuggee a chance for terminating itself
// 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<void> {
return new Promise<void>((resolve, reject) =>
this.initializeSettings(launchArgs)
.then(() => {
logger.log("Launching the application");
logger.verbose(
`Launching the application: ${JSON.stringify(launchArgs, null, 2)}`,
);
this.appLauncher
.launch(launchArgs)
.then(() => {
if (launchArgs.enableDebug) {
launchArgs.port =
launchArgs.port ||
this.appLauncher.getPackagerPort(launchArgs.cwd);
this.attachRequest(response, launchArgs)
.then(() => {
resolve();
})
.catch(e => reject(e));
} else {
this.sendResponse(response);
resolve();
}
})
.catch(err => {
reject(
ErrorHelper.getInternalError(
InternalErrorCode.ApplicationLaunchFailed,
err.message || err,
),
);
});
})
.catch(err => {
reject(
ErrorHelper.getInternalError(
InternalErrorCode.ApplicationLaunchFailed,
err.message || err,
),
);
}),
).catch(err => this.showError(err, response));
}
protected async attachRequest(
response: DebugProtocol.AttachResponse,
attachArgs: IAttachRequestArgs,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
request?: DebugProtocol.Request,
): Promise<void> {
let extProps = {
platform: {
value: attachArgs.platform,
isPii: false,
},
};
this.previousAttachArgs = attachArgs;
return new Promise<void>((resolve, reject) =>
this.initializeSettings(attachArgs)
.then(() => {
logger.log("Attaching to the application");
logger.verbose(
`Attaching to the application: ${JSON.stringify(attachArgs, null, 2)}`,
);
return ProjectVersionHelper.getReactNativeVersions(
this.projectRootPath,
ProjectVersionHelper.generateAdditionalPackagesToCheckByPlatform(
attachArgs,
),
);
})
.then(versions => {
extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties(
attachArgs,
versions,
extProps,
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return TelemetryHelper.generate("attach", extProps, generator => {
attachArgs.port =
attachArgs.port || this.appLauncher.getPackagerPort(attachArgs.cwd);
return this.appLauncher
.getRnCdpProxy()
.stopServer()
.then(() =>
this.appLauncher
.getRnCdpProxy()
.initializeServer(
new RnCDPMessageHandler(),
this.cdpProxyLogLevel,
),
)
.then(() => this.appLauncher.getPackager().start())
.then(() => {
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,
),
);
this.appLauncher.getRnCdpProxy().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;
this.nodeSession.customRequest(this.terminateCommand);
}
}
});
if (this.cancellationTokenSource.token.isCancellationRequested) {
return this.appWorker.stop();
}
return this.appWorker.start();
});
});
})
.catch(err => {
reject(
ErrorHelper.getInternalError(
InternalErrorCode.CouldNotAttachToDebugger,
err.message || err,
),
);
}),
).catch(err => this.showError(err, response));
}
protected async disconnectRequest(
response: DebugProtocol.DisconnectResponse,
args: DebugProtocol.DisconnectArguments,
request?: DebugProtocol.Request,
): Promise<void> {
// 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();
super.disconnectRequest(response, args, request);
}
protected establishDebugSession(
attachArgs: IAttachRequestArgs,
resolve?: (value?: void | PromiseLike<void> | undefined) => void,
): void {
const attachConfiguration = JsDebugConfigAdapter.createDebuggingConfigForPureRN(
attachArgs,
this.appLauncher.getCdpProxyPort(),
this.session.id,
);
vscode.debug
.startDebugging(this.appLauncher.getWorkspaceFolder(), attachConfiguration, {
parentSession: this.session,
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) {
if (
debugSession.configuration.rnDebugSessionId === this.session.id &&
debugSession.type === this.pwaNodeSessionName
) {
this.nodeSession = debugSession;
}
}
private handleTerminateDebugSession(debugSession: vscode.DebugSession) {
if (
debugSession.configuration.rnDebugSessionId === this.session.id &&
debugSession.type === this.pwaNodeSessionName
) {
if (this.debugSessionStatus === DebugSessionStatus.ConnectionPending) {
this.establishDebugSession(this.previousAttachArgs);
} else {
vscode.commands.executeCommand(this.stopCommand, this.session);
}
}
}
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;
}
}
}microsoft/vscode-react-native
Publicmirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable
src/debugger/rnDebugSession.ts
336lines · modepreview