microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
43e1a99628463d630522ffd044ae583fe85de4b3

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

257lines · 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 return this.remoteExtension.launch(request);
80 })
81 .then(() => {
82 return this.remoteExtension.getPackagerPort(request.arguments.program);
83 })
84 .then((packagerPort: number) => {
85 this.attachRequest({
86 ...request,
87 arguments: {
88 ...request.arguments,
89 port: packagerPort,
90 },
91 });
92 })
93 .catch(error => {
94 this.bailOut(error.data || error.message);
95 });
96 }
97
98 private attach(request: DebugProtocol.Request): void {
99 this.requestSetup(request.arguments)
100 .then(() => {
101 return this.remoteExtension.getPackagerPort(request.arguments.program);
102 })
103 .then((packagerPort: number) => {
104 this.attachRequest({
105 ...request,
106 arguments: {
107 ...request.arguments,
108 port: request.arguments.port || packagerPort,
109 },
110 });
111 })
112 .catch(error => {
113 this.bailOut(error.data || error.message);
114 });
115 }
116
117 private disconnect(request: DebugProtocol.Request): void {
118 // The client is about to disconnect so first we need to stop app worker
119 if (this.appWorker) {
120 this.appWorker.stop();
121 }
122
123 // Then we tell the extension to stop monitoring the logcat, and then we disconnect the debugging session
124 if (request.arguments.platform === "android") {
125 this.remoteExtension.stopMonitoringLogcat()
126 .catch(reason => logger.warn(`Couldn't stop monitoring logcat: ${reason.message || reason}`))
127 .finally(() => super.dispatchRequest(request));
128 } else {
129 super.dispatchRequest(request);
130 }
131 }
132
133 private requestSetup(args: any): Q.Promise<void> {
134 let logLevel: string = args.trace;
135 if (logLevel) {
136 logLevel = logLevel.replace(logLevel[0], logLevel[0].toUpperCase());
137 logger.setup(Logger.LogLevel[logLevel], false);
138 } else {
139 logger.setup(Logger.LogLevel.Log, false);
140 }
141
142 const projectRootPath = getProjectRoot(args);
143 return ReactNativeProjectHelper.isReactNativeProject(projectRootPath)
144 .then((result) => {
145 if (!result) {
146 throw new Error(`Seems to be that you are trying to debug from within directory that is not a React Native project root.
147If so, please, follow these instructions: https://github.com/Microsoft/vscode-react-native/blob/master/doc/customization.md#project-structure.`);
148 }
149 this.projectRootPath = projectRootPath;
150 this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
151
152 // Start to send telemetry
153 telemetryReporter.reassignTo(new RemoteTelemetryReporter(
154 appName, version, Telemetry.APPINSIGHTS_INSTRUMENTATIONKEY, this.projectRootPath));
155 return void 0;
156 });
157 }
158
159 /**
160 * Runs logic needed to attach.
161 * Attach should:
162 * - Enable js debugging
163 */
164 // tslint:disable-next-line:member-ordering
165 protected attachRequest(request: DebugProtocol.Request): Q.Promise<void> {
166 const extProps = {
167 platform: {
168 value: request.arguments.platform,
169 isPii: false,
170 },
171 };
172
173 return TelemetryHelper.generate("attach", extProps, (generator) => {
174 return Q({})
175 .then(() => {
176 logger.log("Starting debugger app worker.");
177 // TODO: remove dependency on args.program - "program" property is technically
178 // no more required in launch configuration and could be removed
179 const workspaceRootPath = path.resolve(path.dirname(request.arguments.program), "..");
180 const sourcesStoragePath = path.join(workspaceRootPath, ".vscode", ".react");
181
182 // If launch is invoked first time, appWorker is undefined, so create it here
183 this.appWorker = new MultipleLifetimesAppWorker(
184 request.arguments,
185 sourcesStoragePath,
186 this.projectRootPath,
187 undefined);
188 this.appWorker.on("connected", (port: number) => {
189 logger.log("Debugger worker loaded runtime on port " + port);
190 // Don't mutate original request to avoid side effects
191 let attachArguments = Object.assign({}, request.arguments, {
192 address: "localhost",
193 port,
194 restart: true,
195 request: "attach",
196 remoteRoot: undefined,
197 localRoot: undefined,
198 });
199 // Reinstantiate debug adapter, as the current implementation of ChromeDebugAdapter
200 // doesn't allow us to reattach to another debug target easily. As of now it's easier
201 // to throw previous instance out and create a new one.
202 (this as any)._debugAdapter = new (<any>debugSessionOpts.adapter)(debugSessionOpts, this);
203
204 // Explicity call _debugAdapter.attach() to prevent directly calling dispatchRequest()
205 // yield a response as "attach" even for "launch" request. Because dispatchRequest() will
206 // decide to do a sendResponse() aligning with the request parameter passed in.
207 Q((this as any)._debugAdapter.attach(attachArguments, request.seq))
208 .then((responseBody) => {
209 const response: DebugProtocol.Response = new Response(request);
210 response.body = responseBody;
211 this.sendResponse(response);
212 });
213 });
214
215 return this.appWorker.start();
216 })
217 .catch(error => this.bailOut(error.message));
218 });
219 }
220
221 /**
222 * Logs error to user and finishes the debugging process.
223 */
224 private bailOut(message: string): void {
225 logger.error(`Could not debug. ${message}`);
226 this.sendEvent(new TerminatedEvent());
227 }
228 };
229}
230
231export function makeAdapter(debugAdapterClass: typeof ChromeDebugAdapter): typeof ChromeDebugAdapter {
232 return class extends debugAdapterClass {
233 public doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
234 // We need to overwrite ChromeDebug's _attachMode to let Node2 adapter
235 // to set up breakpoints on initial pause event
236 (this as any)._attachMode = false;
237 return super.doAttach(port, targetUrl, address, timeout);
238 }
239 };
240}
241
242/**
243 * Parses settings.json file for workspace root property
244 */
245function getProjectRoot(args: any): string {
246 try {
247 let vsCodeRoot = path.resolve(args.program, "../..");
248 let settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
249 let settingsContent = fs.readFileSync(settingsPath, "utf8");
250 settingsContent = stripJsonComments(settingsContent);
251 let parsedSettings = JSON.parse(settingsContent);
252 let projectRootPath = parsedSettings["react-native-tools.projectRoot"] || parsedSettings["react-native-tools"].projectRoot;
253 return path.resolve(vsCodeRoot, projectRootPath);
254 } catch (e) {
255 return path.resolve(args.program, "../..");
256 }
257}
258