microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e574eae01cbbbc457f1add5e8e23d40ea0549275

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

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