microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
4c757eeb398e0299d0e9cd9bc95b68dd2f87a06e

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/rnDebugSession.ts

348lines · 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 vscode from "vscode";
5import * as Q from "q";
6import * as path from "path";
7import * as fs from "fs";
8import * as mkdirp from "mkdirp";
9import stripJsonComments = require("strip-json-comments");
10import { LoggingDebugSession, Logger, logger } from "vscode-debugadapter";
11import { DebugProtocol } from "vscode-debugprotocol";
12import { getLoggingDirectory, LogHelper } from "../extension/log/LogHelper";
13import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
14import { ErrorHelper } from "../common/error/errorHelper";
15import { InternalErrorCode } from "../common/error/internalErrorCode";
16import { ILaunchArgs } from "../extension/launchArgs";
17import { ProjectVersionHelper } from "../common/projectVersionHelper";
18import { TelemetryHelper } from "../common/telemetryHelper";
19import { AppLauncher } from "../extension/appLauncher";
20import { MultipleLifetimesAppWorker } from "./appWorker";
21import { ReactNativeCDPProxy } from "../cdp-proxy/reactNativeCDPProxy";
22import { generateRandomPortNumber } from "../common/extensionHelper";
23import { LogLevel } from "../extension/log/LogHelper";
24import * as nls from "vscode-nls";
25const localize = nls.loadMessageBundle();
26
27enum DebugSessionStatus {
28 FirstConnection,
29 FirstConnectionPending,
30 ConnectionAllowed,
31 ConnectionPending,
32 ConnectionDone,
33 ConnectionFailed,
34}
35
36export interface IAttachRequestArgs extends DebugProtocol.AttachRequestArguments, ILaunchArgs {
37 cwd: string; /* Automatically set by VS Code to the currently opened folder */
38 port: number;
39 url?: string;
40 address?: string;
41 trace?: string;
42}
43
44export interface ILaunchRequestArgs extends DebugProtocol.LaunchRequestArguments, IAttachRequestArgs { }
45
46export class RNDebugSession extends LoggingDebugSession {
47
48 private readonly cdpProxyPort: number;
49 private readonly cdpProxyHostAddress: string;
50 private readonly terminateCommand: string;
51 private readonly pwaNodeSessionName: string;
52
53 private appLauncher: AppLauncher;
54 private appWorker: MultipleLifetimesAppWorker | null;
55 private projectRootPath: string;
56 private isSettingsInitialized: boolean; // used to prevent parameters reinitialization when attach is called from launch function
57 private previousAttachArgs: IAttachRequestArgs;
58 private rnCdpProxy: ReactNativeCDPProxy | null;
59 private cdpProxyLogLevel: LogLevel;
60 private nodeSession: vscode.DebugSession | null;
61 private debugSessionStatus: DebugSessionStatus;
62
63 constructor(private session: vscode.DebugSession) {
64 super();
65
66 // constants definition
67 this.cdpProxyPort = generateRandomPortNumber();
68 this.cdpProxyHostAddress = "127.0.0.1"; // localhost
69 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
70 this.pwaNodeSessionName = "pwa-node"; // the name of node debug session created by js-debug extension
71
72 // variables definition
73 this.isSettingsInitialized = false;
74 this.appWorker = null;
75 this.rnCdpProxy = null;
76 this.debugSessionStatus = DebugSessionStatus.FirstConnection;
77
78 vscode.debug.onDidStartDebugSession(
79 this.handleStartDebugSession.bind(this)
80 );
81
82 vscode.debug.onDidTerminateDebugSession(
83 this.handleTerminateDebugSession.bind(this)
84 );
85 }
86
87 protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
88 super.initializeRequest(response, args);
89 }
90
91 protected launchRequest(response: DebugProtocol.LaunchResponse, launchArgs: ILaunchRequestArgs, request?: DebugProtocol.Request): Promise<void> {
92 return new Promise<void>((resolve, reject) => this.initializeSettings(launchArgs)
93 .then(() => {
94 logger.log("Launching the application");
95 logger.verbose(`Launching the application: ${JSON.stringify(launchArgs, null , 2)}`);
96
97 this.appLauncher.launch(launchArgs)
98 .then(() => {
99 return this.appLauncher.getPackagerPort(launchArgs.cwd);
100 })
101 .then((packagerPort: number) => {
102 launchArgs.port = launchArgs.port || packagerPort;
103 this.attachRequest(response, launchArgs).then(() => {
104 resolve();
105 }).catch((e) => reject(e));
106 })
107 .catch((err) => {
108 logger.error("An error occurred while attaching to the debugger. " + err.message || err);
109 reject(err);
110 });
111 }));
112 }
113
114 protected attachRequest(response: DebugProtocol.AttachResponse, attachArgs: IAttachRequestArgs, request?: DebugProtocol.Request): Promise<void> {
115 let extProps = {
116 platform: {
117 value: attachArgs.platform,
118 isPii: false,
119 },
120 };
121
122 this.previousAttachArgs = attachArgs;
123 return new Promise<void>((resolve, reject) => this.initializeSettings(attachArgs)
124 .then(() => {
125 logger.log("Attaching to the application");
126 logger.verbose(`Attaching to the application: ${JSON.stringify(attachArgs, null , 2)}`);
127 return ProjectVersionHelper.getReactNativeVersions(attachArgs.cwd, true)
128 .then(versions => {
129 extProps = TelemetryHelper.addPropertyToTelemetryProperties(versions.reactNativeVersion, "reactNativeVersion", extProps);
130 if (!ProjectVersionHelper.isVersionError(versions.reactNativeWindowsVersion)) {
131 extProps = TelemetryHelper.addPropertyToTelemetryProperties(versions.reactNativeWindowsVersion, "reactNativeWindowsVersion", extProps);
132 }
133 return TelemetryHelper.generate("attach", extProps, (generator) => {
134 this.rnCdpProxy = new ReactNativeCDPProxy(
135 this.cdpProxyHostAddress,
136 this.cdpProxyPort,
137 this.cdpProxyLogLevel
138 );
139 attachArgs.port = attachArgs.port || this.appLauncher.getPackagerPort(attachArgs.cwd);
140 return this.rnCdpProxy.createServer()
141 .then(() => {
142 logger.log(localize("StartingDebuggerAppWorker", "Starting debugger app worker."));
143
144 const sourcesStoragePath = path.join(this.projectRootPath, ".vscode", ".react");
145 // Create folder if not exist to avoid problems if
146 // RN project root is not a ${workspaceFolder}
147 mkdirp.sync(sourcesStoragePath);
148
149 // If launch is invoked first time, appWorker is undefined, so create it here
150 this.appWorker = new MultipleLifetimesAppWorker(
151 attachArgs,
152 sourcesStoragePath,
153 this.projectRootPath,
154 undefined
155 );
156 this.appLauncher.setAppWorker(this.appWorker);
157
158 this.appWorker.on("connected", (port: number) => {
159 logger.log(localize("DebuggerWorkerLoadedRuntimeOnPort", "Debugger worker loaded runtime on port {0}", port));
160
161 if (this.rnCdpProxy) {
162 this.rnCdpProxy.setApplicationTargetPort(port);
163 }
164
165 if (this.debugSessionStatus === DebugSessionStatus.ConnectionPending) {
166 return;
167 }
168
169 if (this.debugSessionStatus === DebugSessionStatus.FirstConnection) {
170 this.debugSessionStatus = DebugSessionStatus.FirstConnectionPending;
171 this.establishDebugSession(resolve);
172 } else if (this.debugSessionStatus === DebugSessionStatus.ConnectionAllowed) {
173 if (this.nodeSession) {
174 this.debugSessionStatus = DebugSessionStatus.ConnectionPending;
175 this.nodeSession.customRequest(this.terminateCommand);
176 }
177 }
178 });
179 return this.appWorker.start();
180 });
181 })
182 .catch((err) => {
183 logger.error("An error occurred while attaching to the debugger. " + err.message || err);
184 reject(err);
185 });
186 });
187 }));
188 }
189
190 protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request): void {
191 // The client is about to disconnect so first we need to stop app worker
192 if (this.appWorker) {
193 this.appWorker.stop();
194 }
195
196 if (this.rnCdpProxy) {
197 this.rnCdpProxy.stopServer();
198 this.rnCdpProxy = null;
199 }
200
201 // Then we tell the extension to stop monitoring the logcat, and then we disconnect the debugging session
202 if (this.previousAttachArgs.platform === "android") {
203 try {
204 this.appLauncher.stopMonitoringLogCat();
205 } catch (err) {
206 logger.warn(localize("CouldNotStopMonitoringLogcat", "Couldn't stop monitoring logcat: {0}", err.message || err));
207 }
208 }
209
210 super.disconnectRequest(response, args, request);
211 }
212
213 private establishDebugSession(resolve?: (value?: void | PromiseLike<void> | undefined) => void): void {
214 if (this.rnCdpProxy) {
215 const attachArguments = {
216 type: "pwa-node",
217 request: "attach",
218 name: "Attach",
219 continueOnAttach: true,
220 port: this.cdpProxyPort,
221 smartStep: false,
222 // The unique identifier of the debug session. It is used to distinguish React Native extension's
223 // debug sessions from other ones. So we can save and process only the extension's debug sessions
224 // in vscode.debug API methods "onDidStartDebugSession" and "onDidTerminateDebugSession".
225 rnDebugSessionId: this.session.id,
226 };
227
228 vscode.debug.startDebugging(
229 this.appLauncher.getWorkspaceFolder(),
230 attachArguments,
231 this.session
232 )
233 .then((childDebugSessionStarted: boolean) => {
234 if (childDebugSessionStarted) {
235 this.debugSessionStatus = DebugSessionStatus.ConnectionDone;
236 this.setConnectionAllowedIfPossible();
237 if (resolve) {
238 this.debugSessionStatus = DebugSessionStatus.ConnectionAllowed;
239 resolve();
240 }
241 } else {
242 this.debugSessionStatus = DebugSessionStatus.ConnectionFailed;
243 this.setConnectionAllowedIfPossible();
244 this.resetFirstConnectionStatus();
245 throw new Error("Cannot start child debug session");
246 }
247 },
248 err => {
249 this.debugSessionStatus = DebugSessionStatus.ConnectionFailed;
250 this.setConnectionAllowedIfPossible();
251 this.resetFirstConnectionStatus();
252 throw err;
253 });
254 } else {
255 this.resetFirstConnectionStatus();
256 throw new Error("Cannot connect to debugger worker: Chrome debugger proxy is offline");
257 }
258 }
259
260 private initializeSettings(args: any): Q.Promise<any> {
261 if (!this.isSettingsInitialized) {
262 let chromeDebugCoreLogs = getLoggingDirectory();
263 if (chromeDebugCoreLogs) {
264 chromeDebugCoreLogs = path.join(chromeDebugCoreLogs, "ChromeDebugCoreLogs.txt");
265 }
266 let logLevel: string = args.trace;
267 if (logLevel) {
268 logLevel = logLevel.replace(logLevel[0], logLevel[0].toUpperCase());
269 logger.setup(Logger.LogLevel[logLevel], chromeDebugCoreLogs || false);
270 this.cdpProxyLogLevel = LogLevel[logLevel] === LogLevel.Verbose ? LogLevel.Custom : LogLevel.None;
271 } else {
272 logger.setup(Logger.LogLevel.Log, chromeDebugCoreLogs || false);
273 this.cdpProxyLogLevel = LogHelper.LOG_LEVEL === LogLevel.Trace ? LogLevel.Custom : LogLevel.None;
274 }
275
276 if (!args.sourceMaps) {
277 args.sourceMaps = true;
278 }
279
280 const projectRootPath = getProjectRoot(args);
281 return ReactNativeProjectHelper.isReactNativeProject(projectRootPath)
282 .then((result) => {
283 if (!result) {
284 throw ErrorHelper.getInternalError(InternalErrorCode.NotInReactNativeFolderError);
285 }
286 this.projectRootPath = projectRootPath;
287 this.appLauncher = AppLauncher.getAppLauncherByProjectRootPath(projectRootPath);
288 this.isSettingsInitialized = true;
289
290 return void 0;
291 });
292 } else {
293 return Q.resolve<void>(void 0);
294 }
295 }
296
297 private handleStartDebugSession(debugSession: vscode.DebugSession) {
298 if (
299 debugSession.configuration.rnDebugSessionId === this.session.id
300 && debugSession.type === this.pwaNodeSessionName
301 ) {
302 this.nodeSession = debugSession;
303 }
304 }
305
306 private handleTerminateDebugSession(debugSession: vscode.DebugSession) {
307 if (
308 debugSession.configuration.rnDebugSessionId === this.session.id
309 && this.debugSessionStatus === DebugSessionStatus.ConnectionPending
310 && debugSession.type === this.pwaNodeSessionName
311 ) {
312 this.establishDebugSession();
313 }
314 }
315
316 private setConnectionAllowedIfPossible(): void {
317 if (
318 this.debugSessionStatus === DebugSessionStatus.ConnectionDone
319 || this.debugSessionStatus === DebugSessionStatus.ConnectionFailed
320 ) {
321 this.debugSessionStatus = DebugSessionStatus.ConnectionAllowed;
322 }
323 }
324
325 private resetFirstConnectionStatus(): void {
326 if (this.debugSessionStatus === DebugSessionStatus.FirstConnectionPending) {
327 this.debugSessionStatus = DebugSessionStatus.FirstConnection;
328 }
329 }
330}
331
332/**
333 * Parses settings.json file for workspace root property
334 */
335export function getProjectRoot(args: any): string {
336 const vsCodeRoot = args.cwd ? path.resolve(args.cwd) : path.resolve(args.program, "../..");
337 const settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
338 try {
339 let settingsContent = fs.readFileSync(settingsPath, "utf8");
340 settingsContent = stripJsonComments(settingsContent);
341 let parsedSettings = JSON.parse(settingsContent);
342 let projectRootPath = parsedSettings["react-native-tools.projectRoot"] || parsedSettings["react-native-tools"].projectRoot;
343 return path.resolve(vsCodeRoot, projectRootPath);
344 } catch (e) {
345 logger.verbose(`${settingsPath} file doesn't exist or its content is incorrect. This file will be ignored.`);
346 return args.cwd ? path.resolve(args.cwd) : path.resolve(args.program, "../..");
347 }
348}
349