microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
14ebf4e6fdee5f69a41d9a7deea4dc164dd28b7e

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

251lines · 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(
166 request: DebugProtocol.Request): Q.Promise<void> {
167 return TelemetryHelper.generate(request.command, (generator) => {
168 return Q({})
169 .then(() => {
170 logger.log("Starting debugger app worker.");
171 // TODO: remove dependency on args.program - "program" property is technically
172 // no more required in launch configuration and could be removed
173 const workspaceRootPath = path.resolve(path.dirname(request.arguments.program), "..");
174 const sourcesStoragePath = path.join(workspaceRootPath, ".vscode", ".react");
175
176 // If launch is invoked first time, appWorker is undefined, so create it here
177 this.appWorker = new MultipleLifetimesAppWorker(
178 request.arguments,
179 sourcesStoragePath,
180 this.projectRootPath,
181 undefined);
182 this.appWorker.on("connected", (port: number) => {
183 logger.log("Debugger worker loaded runtime on port " + port);
184 // Don't mutate original request to avoid side effects
185 let attachArguments = Object.assign({}, request.arguments, {
186 address: "localhost",
187 port,
188 restart: true,
189 request: "attach",
190 remoteRoot: undefined,
191 localRoot: undefined,
192 });
193 // Reinstantiate debug adapter, as the current implementation of ChromeDebugAdapter
194 // doesn't allow us to reattach to another debug target easily. As of now it's easier
195 // to throw previous instance out and create a new one.
196 (this as any)._debugAdapter = new (<any>debugSessionOpts.adapter)(debugSessionOpts, this);
197
198 // Explicity call _debugAdapter.attach() to prevent directly calling dispatchRequest()
199 // yield a response as "attach" even for "launch" request. Because dispatchRequest() will
200 // decide to do a sendResponse() aligning with the request parameter passed in.
201 Q((this as any)._debugAdapter.attach(attachArguments, request.seq))
202 .then((responseBody) => {
203 const response: DebugProtocol.Response = new Response(request);
204 response.body = responseBody;
205 this.sendResponse(response);
206 });
207 });
208
209 return this.appWorker.start();
210 })
211 .catch(error => this.bailOut(error.message));
212 });
213 }
214
215 /**
216 * Logs error to user and finishes the debugging process.
217 */
218 private bailOut(message: string): void {
219 logger.error(`Could not debug. ${message}`);
220 this.sendEvent(new TerminatedEvent());
221 }
222 };
223}
224
225export function makeAdapter(debugAdapterClass: typeof ChromeDebugAdapter): typeof ChromeDebugAdapter {
226 return class extends debugAdapterClass {
227 public doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
228 // We need to overwrite ChromeDebug's _attachMode to let Node2 adapter
229 // to set up breakpoints on initial pause event
230 (this as any)._attachMode = false;
231 return super.doAttach(port, targetUrl, address, timeout);
232 }
233 };
234}
235
236/**
237 * Parses settings.json file for workspace root property
238 */
239function getProjectRoot(args: any): string {
240 try {
241 let vsCodeRoot = path.resolve(args.program, "../..");
242 let settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
243 let settingsContent = fs.readFileSync(settingsPath, "utf8");
244 settingsContent = stripJsonComments(settingsContent);
245 let parsedSettings = JSON.parse(settingsContent);
246 let projectRootPath = parsedSettings["react-native-tools.projectRoot"] || parsedSettings["react-native-tools"].projectRoot;
247 return path.resolve(vsCodeRoot, projectRootPath);
248 } catch (e) {
249 return path.resolve(args.program, "../..");
250 }
251}