microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
70cfd5650973cc6872aaae01ea2db7a924e6b67b

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

316lines · 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.getReactNativeVersion(request.arguments.cwd)
198 .then(version => {
199 extProps = TelemetryHelper.addReactNativeVersionToEventProperties(version, extProps);
200 return TelemetryHelper.generate("attach", extProps, (generator) => {
201 return Q({})
202 .then(() => {
203 logger.log(localize("StartingDebuggerAppWorker", "Starting debugger app worker."));
204 // TODO: remove dependency on args.program - "program" property is technically
205 // no more required in launch configuration and could be removed
206 const workspaceRootPath = request.arguments.cwd ? path.resolve(request.arguments.cwd) : path.resolve(path.dirname(request.arguments.program), "..");
207 const sourcesStoragePath = path.join(workspaceRootPath, ".vscode", ".react");
208 // Create folder if not exist to avoid problems if
209 // RN project root is not a ${workspaceFolder}
210 mkdirp.sync(sourcesStoragePath);
211
212 // If launch is invoked first time, appWorker is undefined, so create it here
213 this.appWorker = new MultipleLifetimesAppWorker(
214 request.arguments,
215 sourcesStoragePath,
216 this.projectRootPath,
217 undefined);
218 this.appWorker.on("connected", (port: number) => {
219 logger.log(localize("DebuggerWorkerLoadedRuntimeOnPort", "Debugger worker loaded runtime on port {0}", port));
220 // Don't mutate original request to avoid side effects
221 let attachArguments = Object.assign({}, request.arguments, {
222 address: "localhost",
223 port,
224 restart: true,
225 request: "attach",
226 remoteRoot: undefined,
227 localRoot: undefined,
228 });
229 // Reinstantiate debug adapter, as the current implementation of ChromeDebugAdapter
230 // doesn't allow us to reattach to another debug target easily. As of now it's easier
231 // to throw previous instance out and create a new one.
232 (this as any)._debugAdapter = new (<any>debugSessionOpts.adapter)(debugSessionOpts, this);
233
234 // Explicity call _debugAdapter.attach() to prevent directly calling dispatchRequest()
235 // yield a response as "attach" even for "launch" request. Because dispatchRequest() will
236 // decide to do a sendResponse() aligning with the request parameter passed in.
237 Q((this as any)._debugAdapter.attach(attachArguments, request.seq))
238 .then((responseBody) => {
239 const response: DebugProtocol.Response = new Response(request);
240 response.body = responseBody;
241 this.sendResponse(response);
242 });
243 });
244
245 return this.appWorker.start();
246 })
247 .catch(error => this.bailOut(error.message));
248 });
249 });
250 }
251
252 /**
253 * Logs error to user and finishes the debugging process.
254 */
255 private bailOut(message: string): void {
256 logger.error(localize("CouldNotDebug", "Could not debug. {0}" , message));
257 this.sendEvent(new TerminatedEvent());
258 }
259 };
260}
261
262export function makeAdapter(debugAdapterClass: typeof ChromeDebugAdapter): typeof ChromeDebugAdapter {
263 return class extends debugAdapterClass {
264 private firstStop: boolean = true;
265 public doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
266 // We need to overwrite ChromeDebug's _attachMode to let Node2 adapter
267 // to set up breakpoints on initial pause event
268 (this as any)._attachMode = false;
269 return super.doAttach(port, targetUrl, address, timeout);
270 }
271
272 // Since the bundle runs inside the Node.js VM in debuggerWorker.js in runtime
273 // Node debug adapter need time to parse new added code source maps
274 // So we added 'debugger;' statement at the start of the bundle code
275 // and wait for the adapter to receive a signal to stop on that statement
276 // and then wait for code bundle to be processed and then send continue request to skip the code execution stop in VS Code UI
277 public onPaused(notification: Crdp.Debugger.PausedEvent, expectingStopReason?: stoppedEvent.ReasonType): Promise<IOnPausedResult> {
278 // When pause on 'debugger;' statement, notification contains reason with value "other" instead of "breakpoint"
279 if (this.firstStop && notification.reason === "other") {
280 return new Promise<IOnPausedResult>((resolve) => {
281 setTimeout(() => {
282 this.firstStop = false;
283 this.continue();
284 resolve({didPause: false});
285 }, 50);
286 });
287 } else {
288 return super.onPaused(notification, expectingStopReason);
289 }
290 }
291
292 public async terminate(args: DebugProtocol.TerminatedEvent) {
293 return this.disconnect({
294 terminateDebuggee: true,
295 });
296 }
297 };
298}
299
300/**
301 * Parses settings.json file for workspace root property
302 */
303export function getProjectRoot(args: any): string {
304 const vsCodeRoot = args.cwd ? path.resolve(args.cwd) : path.resolve(args.program, "../..");
305 const settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
306 try {
307 let settingsContent = fs.readFileSync(settingsPath, "utf8");
308 settingsContent = stripJsonComments(settingsContent);
309 let parsedSettings = JSON.parse(settingsContent);
310 let projectRootPath = parsedSettings["react-native-tools.projectRoot"] || parsedSettings["react-native-tools"].projectRoot;
311 return path.resolve(vsCodeRoot, projectRootPath);
312 } catch (e) {
313 logger.verbose(`${settingsPath} file doesn't exist or its content is incorrect. This file will be ignored.`);
314 return args.cwd ? path.resolve(args.cwd) : path.resolve(args.program, "../..");
315 }
316}
317