microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
10f9b91495f3516e9f5ae2e861a52bf4817d4fb8

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

265lines · 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");
8
9import { Telemetry } from "../common/telemetry";
10import { TelemetryHelper } from "../common/telemetryHelper";
11import { RemoteExtension } from "../common/remoteExtension";
12import { RemoteTelemetryReporter, ReassignableTelemetryReporter } from "../common/telemetryReporters";
13import { ChromeDebugSession, IChromeDebugSessionOpts, ChromeDebugAdapter, logger } from "vscode-chrome-debug-core";
14import { ContinuedEvent, TerminatedEvent, Logger, Response } from "vscode-debugadapter";
15import { DebugProtocol } from "vscode-debugprotocol";
16
17import { MultipleLifetimesAppWorker } from "./appWorker";
18
19import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
20
21
22export function makeSession(
23 debugSessionClass: typeof ChromeDebugSession,
24 debugSessionOpts: IChromeDebugSessionOpts,
25 telemetryReporter: ReassignableTelemetryReporter,
26 appName: string, version: string): typeof ChromeDebugSession {
27
28 return class extends debugSessionClass {
29
30 private projectRootPath: string;
31 private remoteExtension: RemoteExtension;
32 private appWorker: MultipleLifetimesAppWorker | null = null;
33
34 constructor(debuggerLinesAndColumnsStartAt1?: boolean, isServer?: boolean) {
35 super(debuggerLinesAndColumnsStartAt1, isServer, debugSessionOpts);
36 }
37
38 // Override ChromeDebugSession's sendEvent to control what we will send to client
39 public sendEvent(event: DebugProtocol.Event): void {
40 // Do not send "terminated" events signaling about session's restart to client as it would cause it
41 // to restart adapter's process, while we want to stay alive and don't want to interrupt connection
42 // to packager.
43
44 if (event.event === "terminated" && event.body && event.body.restart) {
45
46 // Worker has been reloaded and switched to "continue" state
47 // So we have to send "continued" event to client instead of "terminated"
48 // Otherwise client might mistakenly show "stopped" state
49 let continuedEvent: ContinuedEvent = {
50 event: "continued",
51 type: "event",
52 seq: event["seq"], // tslint:disable-line
53 body: { threadId: event.body.threadId },
54 };
55
56 super.sendEvent(continuedEvent);
57 return;
58 }
59
60 super.sendEvent(event);
61 }
62
63 protected dispatchRequest(request: DebugProtocol.Request): void {
64 if (request.command === "disconnect")
65 return this.disconnect(request);
66
67 if (request.command === "attach")
68 return this.attach(request);
69
70 if (request.command === "launch")
71 return this.launch(request);
72
73 return super.dispatchRequest(request);
74 }
75
76 private launch(request: DebugProtocol.Request): void {
77 this.requestSetup(request.arguments)
78 .then(() => {
79 logger.verbose(`Handle launch request: ${JSON.stringify(request.arguments, null , 2)}`);
80 return this.remoteExtension.launch(request);
81 })
82 .then(() => {
83 return this.remoteExtension.getPackagerPort(request.arguments.program);
84 })
85 .then((packagerPort: number) => {
86 this.attachRequest({
87 ...request,
88 arguments: {
89 ...request.arguments,
90 port: packagerPort,
91 },
92 });
93 })
94 .catch(error => {
95 this.bailOut(error.data || error.message);
96 });
97 }
98
99 private attach(request: DebugProtocol.Request): void {
100 this.requestSetup(request.arguments)
101 .then(() => {
102 logger.verbose(`Handle attach request: ${request.arguments}`);
103 return this.remoteExtension.getPackagerPort(request.arguments.program);
104 })
105 .then((packagerPort: number) => {
106 this.attachRequest({
107 ...request,
108 arguments: {
109 ...request.arguments,
110 port: request.arguments.port || packagerPort,
111 },
112 });
113 })
114 .catch(error => {
115 this.bailOut(error.data || error.message);
116 });
117 }
118
119 private disconnect(request: DebugProtocol.Request): void {
120 // The client is about to disconnect so first we need to stop app worker
121 if (this.appWorker) {
122 this.appWorker.stop();
123 }
124
125 // Then we tell the extension to stop monitoring the logcat, and then we disconnect the debugging session
126 if (request.arguments.platform === "android") {
127 this.remoteExtension.stopMonitoringLogcat()
128 .catch(reason => logger.warn(`Couldn't stop monitoring logcat: ${reason.message || reason}`))
129 .finally(() => super.dispatchRequest(request));
130 } else {
131 super.dispatchRequest(request);
132 }
133 }
134
135 private requestSetup(args: any): Q.Promise<void> {
136 let logLevel: string = args.trace;
137 if (logLevel) {
138 logLevel = logLevel.replace(logLevel[0], logLevel[0].toUpperCase());
139 logger.setup(Logger.LogLevel[logLevel], false);
140 } else {
141 logger.setup(Logger.LogLevel.Log, false);
142 }
143
144 const projectRootPath = getProjectRoot(args);
145 return ReactNativeProjectHelper.isReactNativeProject(projectRootPath)
146 .then((result) => {
147 if (!result) {
148 throw new Error(`Seems to be that you are trying to debug from within directory that is not a React Native project root.
149If so, please, follow these instructions: https://github.com/Microsoft/vscode-react-native/blob/master/doc/customization.md#project-structure.`);
150 }
151 this.projectRootPath = projectRootPath;
152 this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
153
154 // Start to send telemetry
155 telemetryReporter.reassignTo(new RemoteTelemetryReporter(
156 appName, version, Telemetry.APPINSIGHTS_INSTRUMENTATIONKEY, this.projectRootPath));
157 return void 0;
158 });
159 }
160
161 /**
162 * Runs logic needed to attach.
163 * Attach should:
164 * - Enable js debugging
165 */
166 // tslint:disable-next-line:member-ordering
167 protected attachRequest(request: DebugProtocol.Request): Q.Promise<void> {
168 const extProps = {
169 platform: {
170 value: request.arguments.platform,
171 isPii: false,
172 },
173 };
174
175 return TelemetryHelper.generate("attach", extProps, (generator) => {
176 return Q({})
177 .then(() => {
178 logger.log("Starting debugger app worker.");
179 // TODO: remove dependency on args.program - "program" property is technically
180 // no more required in launch configuration and could be removed
181 const workspaceRootPath = path.resolve(path.dirname(request.arguments.program), "..");
182 const sourcesStoragePath = path.join(workspaceRootPath, ".vscode", ".react");
183
184 // If launch is invoked first time, appWorker is undefined, so create it here
185 this.appWorker = new MultipleLifetimesAppWorker(
186 request.arguments,
187 sourcesStoragePath,
188 this.projectRootPath,
189 undefined);
190 this.appWorker.on("connected", (port: number) => {
191 logger.log("Debugger worker loaded runtime on port " + port);
192 // Don't mutate original request to avoid side effects
193 let attachArguments = Object.assign({}, request.arguments, {
194 address: "localhost",
195 port,
196 restart: true,
197 request: "attach",
198 remoteRoot: undefined,
199 localRoot: undefined,
200 });
201 // Reinstantiate debug adapter, as the current implementation of ChromeDebugAdapter
202 // doesn't allow us to reattach to another debug target easily. As of now it's easier
203 // to throw previous instance out and create a new one.
204 (this as any)._debugAdapter = new (<any>debugSessionOpts.adapter)(debugSessionOpts, this);
205
206 // Explicity call _debugAdapter.attach() to prevent directly calling dispatchRequest()
207 // yield a response as "attach" even for "launch" request. Because dispatchRequest() will
208 // decide to do a sendResponse() aligning with the request parameter passed in.
209 Q((this as any)._debugAdapter.attach(attachArguments, request.seq))
210 .then((responseBody) => {
211 const response: DebugProtocol.Response = new Response(request);
212 response.body = responseBody;
213 this.sendResponse(response);
214 });
215 });
216
217 return this.appWorker.start();
218 })
219 .catch(error => this.bailOut(error.message));
220 });
221 }
222
223 /**
224 * Logs error to user and finishes the debugging process.
225 */
226 private bailOut(message: string): void {
227 logger.error(`Could not debug. ${message}`);
228 this.sendEvent(new TerminatedEvent());
229 }
230 };
231}
232
233export function makeAdapter(debugAdapterClass: typeof ChromeDebugAdapter): typeof ChromeDebugAdapter {
234 return class extends debugAdapterClass {
235 public doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
236 // We need to overwrite ChromeDebug's _attachMode to let Node2 adapter
237 // to set up breakpoints on initial pause event
238 (this as any)._attachMode = false;
239 return super.doAttach(port, targetUrl, address, timeout);
240 }
241
242 public async terminate(args: DebugProtocol.TerminatedEvent) {
243 return this.disconnect({
244 terminateDebuggee: true,
245 });
246 }
247 };
248}
249
250/**
251 * Parses settings.json file for workspace root property
252 */
253function getProjectRoot(args: any): string {
254 try {
255 let vsCodeRoot = path.resolve(args.program, "../..");
256 let settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
257 let settingsContent = fs.readFileSync(settingsPath, "utf8");
258 settingsContent = stripJsonComments(settingsContent);
259 let parsedSettings = JSON.parse(settingsContent);
260 let projectRootPath = parsedSettings["react-native-tools.projectRoot"] || parsedSettings["react-native-tools"].projectRoot;
261 return path.resolve(vsCodeRoot, projectRootPath);
262 } catch (e) {
263 return path.resolve(args.program, "../..");
264 }
265}
266