microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
36e9730fc3e5568605829ee7ab2d402bca4c18ac

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

320lines · 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 Q from "q";
5import * as path from "path";
6import * as fs from "fs";
7import stripJsonComments = require("strip-json-comments");
8import { Telemetry } from "../common/telemetry";
9import { TelemetryHelper } from "../common/telemetryHelper";
10import { RemoteExtension } from "../common/remoteExtension";
11import { RemoteTelemetryReporter, ReassignableTelemetryReporter } from "../common/telemetryReporters";
12import { ChromeDebugSession, IChromeDebugSessionOpts, ChromeDebugAdapter, logger, Crdp, stoppedEvent, IOnPausedResult } from "vscode-chrome-debug-core";
13import { ContinuedEvent, TerminatedEvent, Logger, Response } from "vscode-debugadapter";
14import { DebugProtocol } from "vscode-debugprotocol";
15import { MultipleLifetimesAppWorker } from "./appWorker";
16import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
17import { ProjectVersionHelper } from "../common/projectVersionHelper";
18import * as nls from "vscode-nls";
19import { ErrorHelper } from "../common/error/errorHelper";
20import { InternalErrorCode } from "../common/error/internalErrorCode";
21import { getLoggingDirectory } from "../extension/log/LogHelper";
22import * as mkdirp from "mkdirp";
23const localize = nls.loadMessageBundle();
24
25export function makeSession(
26 debugSessionClass: typeof ChromeDebugSession,
27 debugSessionOpts: IChromeDebugSessionOpts,
28 telemetryReporter: ReassignableTelemetryReporter,
29 appName: string, version: string): typeof ChromeDebugSession {
30
31 return class extends debugSessionClass {
32
33 private projectRootPath: string;
34 private remoteExtension: RemoteExtension;
35 private appWorker: MultipleLifetimesAppWorker | null = null;
36
37 constructor(debuggerLinesAndColumnsStartAt1?: boolean, isServer?: boolean) {
38 super(debuggerLinesAndColumnsStartAt1, isServer, debugSessionOpts);
39 }
40
41 // Override ChromeDebugSession's sendEvent to control what we will send to client
42 public sendEvent(event: DebugProtocol.Event): void {
43 // Do not send "terminated" events signaling about session's restart to client as it would cause it
44 // to restart adapter's process, while we want to stay alive and don't want to interrupt connection
45 // to packager.
46
47 if (event.event === "terminated" && event.body && event.body.restart) {
48
49 // Worker has been reloaded and switched to "continue" state
50 // So we have to send "continued" event to client instead of "terminated"
51 // Otherwise client might mistakenly show "stopped" state
52 let continuedEvent: ContinuedEvent = {
53 event: "continued",
54 type: "event",
55 seq: event["seq"], // tslint:disable-line
56 body: { threadId: event.body.threadId },
57 };
58
59 super.sendEvent(continuedEvent);
60 return;
61 }
62
63 super.sendEvent(event);
64 }
65
66 protected dispatchRequest(request: DebugProtocol.Request): void {
67 if (request.command === "disconnect")
68 return this.disconnect(request);
69
70 if (request.command === "attach")
71 return this.attach(request);
72
73 if (request.command === "launch")
74 return this.launch(request);
75
76 return super.dispatchRequest(request);
77 }
78
79 private launch(request: DebugProtocol.Request): void {
80 this.requestSetup(request.arguments)
81 .then(() => {
82 logger.verbose(`Handle launch request: ${JSON.stringify(request.arguments, null , 2)}`);
83 return this.remoteExtension.launch(request);
84 })
85 .then(() => {
86 return this.remoteExtension.getPackagerPort(request.arguments.cwd || request.arguments.program);
87 })
88 .then((packagerPort: number) => {
89 this.attachRequest({
90 ...request,
91 arguments: {
92 ...request.arguments,
93 port: packagerPort,
94 },
95 });
96 })
97 .catch(error => {
98 this.bailOut(error.data || error.message);
99 });
100 }
101
102 private attach(request: DebugProtocol.Request): void {
103 this.requestSetup(request.arguments)
104 .then(() => {
105 logger.verbose(`Handle attach request: ${JSON.stringify(request.arguments, null , 2)}`);
106 return this.remoteExtension.getPackagerPort(request.arguments.cwd || request.arguments.program);
107 })
108 .then((packagerPort: number) => {
109 this.attachRequest({
110 ...request,
111 arguments: {
112 ...request.arguments,
113 port: request.arguments.port || packagerPort,
114 },
115 });
116 })
117 .catch(error => {
118 this.bailOut(error.data || error.message);
119 });
120 }
121
122 private disconnect(request: DebugProtocol.Request): void {
123 // The client is about to disconnect so first we need to stop app worker
124 if (this.appWorker) {
125 this.appWorker.stop();
126 }
127
128 // Then we tell the extension to stop monitoring the logcat, and then we disconnect the debugging session
129 if (request.arguments.platform === "android") {
130 this.remoteExtension.stopMonitoringLogcat()
131 .catch(reason => logger.warn(localize("CouldNotStopMonitoringLogcat", "Couldn't stop monitoring logcat: {0}", reason.message || reason)))
132 .finally(() => super.dispatchRequest(request));
133 } else {
134 super.dispatchRequest(request);
135 }
136 }
137
138 private requestSetup(args: any): Q.Promise<void> {
139 // If special env variables are defined, then write process outputs to file
140 let chromeDebugCoreLogs = getLoggingDirectory();
141 if (chromeDebugCoreLogs) {
142 chromeDebugCoreLogs = path.join(chromeDebugCoreLogs, "ChromeDebugCoreLogs.txt");
143 }
144 let logLevel: string = args.trace;
145 if (logLevel) {
146 logLevel = logLevel.replace(logLevel[0], logLevel[0].toUpperCase());
147 logger.setup(Logger.LogLevel[logLevel], chromeDebugCoreLogs || false);
148 } else {
149 logger.setup(Logger.LogLevel.Log, chromeDebugCoreLogs || false);
150 }
151
152
153 if (!args.sourceMaps) {
154 args.sourceMaps = true;
155 }
156 const projectRootPath = getProjectRoot(args);
157 return ReactNativeProjectHelper.isReactNativeProject(projectRootPath)
158 .then((result) => {
159 if (!result) {
160 throw ErrorHelper.getInternalError(InternalErrorCode.NotInReactNativeFolderError);
161 }
162 this.projectRootPath = projectRootPath;
163 this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
164
165 // Start to send telemetry
166 telemetryReporter.reassignTo(new RemoteTelemetryReporter(
167 appName, version, Telemetry.APPINSIGHTS_INSTRUMENTATIONKEY, this.projectRootPath));
168
169 if (args.program) {
170 // TODO: Remove this warning when program property will be completely removed
171 logger.warn(localize("ProgramPropertyDeprecationWarning", "Launched debug configuration contains 'program' property which is deprecated and will be removed soon. Please replace it with: \"cwd\": \"${workspaceFolder}\""));
172 const useProgramEvent = TelemetryHelper.createTelemetryEvent("useProgramProperty");
173 Telemetry.send(useProgramEvent);
174 }
175 if (args.cwd) {
176 // To match count of 'cwd' users with 'program' users. TODO: Remove when program property will be removed
177 const useCwdEvent = TelemetryHelper.createTelemetryEvent("useCwdProperty");
178 Telemetry.send(useCwdEvent);
179 }
180 return void 0;
181 });
182 }
183
184 /**
185 * Runs logic needed to attach.
186 * Attach should:
187 * - Enable js debugging
188 */
189 // tslint:disable-next-line:member-ordering
190 protected attachRequest(request: DebugProtocol.Request): Q.Promise<void> {
191 let extProps = {
192 platform: {
193 value: request.arguments.platform,
194 isPii: false,
195 },
196 };
197
198 return ProjectVersionHelper.getReactNativeVersions(request.arguments.cwd, true)
199 .then(versions => {
200 extProps = TelemetryHelper.addPropertyToTelemetryProperties(versions.reactNativeVersion, "reactNativeVersion", extProps);
201 if (!ProjectVersionHelper.isVersionError(versions.reactNativeWindowsVersion)) {
202 extProps = TelemetryHelper.addPropertyToTelemetryProperties(versions.reactNativeWindowsVersion, "reactNativeWindowsVersion", extProps);
203 }
204 return TelemetryHelper.generate("attach", extProps, (generator) => {
205 return Q({})
206 .then(() => {
207 logger.log(localize("StartingDebuggerAppWorker", "Starting debugger app worker."));
208 // TODO: remove dependency on args.program - "program" property is technically
209 // no more required in launch configuration and could be removed
210 const workspaceRootPath = request.arguments.cwd ? path.resolve(request.arguments.cwd) : path.resolve(path.dirname(request.arguments.program), "..");
211 const sourcesStoragePath = path.join(workspaceRootPath, ".vscode", ".react");
212 // Create folder if not exist to avoid problems if
213 // RN project root is not a ${workspaceFolder}
214 mkdirp.sync(sourcesStoragePath);
215
216 // If launch is invoked first time, appWorker is undefined, so create it here
217 this.appWorker = new MultipleLifetimesAppWorker(
218 request.arguments,
219 sourcesStoragePath,
220 this.projectRootPath,
221 undefined);
222 this.appWorker.on("connected", (port: number) => {
223 logger.log(localize("DebuggerWorkerLoadedRuntimeOnPort", "Debugger worker loaded runtime on port {0}", port));
224 // Don't mutate original request to avoid side effects
225 let attachArguments = Object.assign({}, request.arguments, {
226 address: "localhost",
227 port,
228 restart: true,
229 request: "attach",
230 remoteRoot: undefined,
231 localRoot: undefined,
232 });
233 // Reinstantiate debug adapter, as the current implementation of ChromeDebugAdapter
234 // doesn't allow us to reattach to another debug target easily. As of now it's easier
235 // to throw previous instance out and create a new one.
236 (this as any)._debugAdapter = new (<any>debugSessionOpts.adapter)(debugSessionOpts, this);
237
238 // Explicity call _debugAdapter.attach() to prevent directly calling dispatchRequest()
239 // yield a response as "attach" even for "launch" request. Because dispatchRequest() will
240 // decide to do a sendResponse() aligning with the request parameter passed in.
241 Q((this as any)._debugAdapter.attach(attachArguments, request.seq))
242 .then((responseBody) => {
243 const response: DebugProtocol.Response = new Response(request);
244 response.body = responseBody;
245 this.sendResponse(response);
246 });
247 });
248
249 return this.appWorker.start();
250 })
251 .catch(error => this.bailOut(error.message));
252 });
253 });
254 }
255
256 /**
257 * Logs error to user and finishes the debugging process.
258 */
259 private bailOut(message: string): void {
260 logger.error(localize("CouldNotDebug", "Could not debug. {0}" , message));
261 this.sendEvent(new TerminatedEvent());
262 }
263 };
264}
265
266export function makeAdapter(debugAdapterClass: typeof ChromeDebugAdapter): typeof ChromeDebugAdapter {
267 return class extends debugAdapterClass {
268 private firstStop: boolean = true;
269 public doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
270 // We need to overwrite ChromeDebug's _attachMode to let Node2 adapter
271 // to set up breakpoints on initial pause event
272 (this as any)._attachMode = false;
273 return super.doAttach(port, targetUrl, address, timeout);
274 }
275
276 // Since the bundle runs inside the Node.js VM in debuggerWorker.js in runtime
277 // Node debug adapter need time to parse new added code source maps
278 // So we added 'debugger;' statement at the start of the bundle code
279 // and wait for the adapter to receive a signal to stop on that statement
280 // and then wait for code bundle to be processed and then send continue request to skip the code execution stop in VS Code UI
281 public onPaused(notification: Crdp.Debugger.PausedEvent, expectingStopReason?: stoppedEvent.ReasonType): Promise<IOnPausedResult> {
282 // When pause on 'debugger;' statement, notification contains reason with value "other" instead of "breakpoint"
283 if (this.firstStop && notification.reason === "other") {
284 return new Promise<IOnPausedResult>((resolve) => {
285 setTimeout(() => {
286 this.firstStop = false;
287 this.continue();
288 resolve({didPause: false});
289 }, 50);
290 });
291 } else {
292 return super.onPaused(notification, expectingStopReason);
293 }
294 }
295
296 public async terminate(args: DebugProtocol.TerminatedEvent) {
297 return this.disconnect({
298 terminateDebuggee: true,
299 });
300 }
301 };
302}
303
304/**
305 * Parses settings.json file for workspace root property
306 */
307export function getProjectRoot(args: any): string {
308 const vsCodeRoot = args.cwd ? path.resolve(args.cwd) : path.resolve(args.program, "../..");
309 const settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
310 try {
311 let settingsContent = fs.readFileSync(settingsPath, "utf8");
312 settingsContent = stripJsonComments(settingsContent);
313 let parsedSettings = JSON.parse(settingsContent);
314 let projectRootPath = parsedSettings["react-native-tools.projectRoot"] || parsedSettings["react-native-tools"].projectRoot;
315 return path.resolve(vsCodeRoot, projectRootPath);
316 } catch (e) {
317 logger.verbose(`${settingsPath} file doesn't exist or its content is incorrect. This file will be ignored.`);
318 return args.cwd ? path.resolve(args.cwd) : path.resolve(args.program, "../..");
319 }
320}
321