microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
b926320f325252635886363418fb15f5d8d58890

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

276lines · 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 http from "http";
7import * as fs from "fs";
8import stripJsonComments = require("strip-json-comments");
9
10import {Telemetry} from "../common/telemetry";
11import {TelemetryHelper} from "../common/telemetryHelper";
12import {RemoteExtension} from "../common/remoteExtension";
13import {IOSPlatform} from "./ios/iOSPlatform";
14import {PlatformResolver} from "./platformResolver";
15import {IRunOptions} from "../common/launchArgs";
16import {TargetPlatformHelper} from "../common/targetPlatformHelper";
17import {ExtensionTelemetryReporter, ReassignableTelemetryReporter} from "../common/telemetryReporters";
18import {NodeDebugAdapterLogger} from "../common/log/loggers";
19import {Log} from "../common/log/log";
20import {GeneralMobilePlatform} from "../common/generalMobilePlatform";
21
22export class NodeDebugWrapper {
23 private projectRootPath: string;
24 private remoteExtension: RemoteExtension;
25 private telemetryReporter: ReassignableTelemetryReporter;
26 private appName: string;
27 private version: string;
28 private mobilePlatformOptions: IRunOptions;
29
30 private vscodeDebugAdapterPackage: typeof VSCodeDebugAdapter;
31 private nodeDebugSession: typeof NodeDebugSession;
32 private sourceMapsConstructor: typeof SourceMaps;
33 private originalLaunchRequest: (response: any, args: any) => void;
34
35 public constructor(appName: string, version: string, telemetryReporter: ReassignableTelemetryReporter,
36 debugAdapter: typeof VSCodeDebugAdapter, debugSession: typeof NodeDebugSession, sourceMaps: typeof SourceMaps) {
37
38 this.appName = appName;
39 this.version = version;
40 this.telemetryReporter = telemetryReporter;
41 this.vscodeDebugAdapterPackage = debugAdapter;
42 this.nodeDebugSession = debugSession;
43 this.sourceMapsConstructor = sourceMaps;
44 this.originalLaunchRequest = this.nodeDebugSession.prototype.launchRequest;
45 }
46
47 /**
48 * Calls customize methods for all requests needed
49 */
50 public customizeNodeAdapterRequests(): void {
51 this.customizeLaunchRequest();
52 this.customizeAttachRequest();
53 this.customizeDisconnectRequest();
54 }
55
56 /**
57 * Intecept the "launchRequest" instance method of NodeDebugSession to interpret arguments.
58 * Launch should:
59 * - Run the packager if needed
60 * - Compile and run application
61 * - Prewarm bundle
62 */
63 private customizeLaunchRequest(): void {
64 const nodeDebugWrapper = this;
65 this.nodeDebugSession.prototype.launchRequest = function (request: any, args: ILaunchRequestArgs) {
66 nodeDebugWrapper.requestSetup(this, args);
67 nodeDebugWrapper.mobilePlatformOptions.target = args.target || "simulator";
68 nodeDebugWrapper.mobilePlatformOptions.iosRelativeProjectPath = !nodeDebugWrapper.isNullOrUndefined(args.iosRelativeProjectPath) ?
69 args.iosRelativeProjectPath :
70 IOSPlatform.DEFAULT_IOS_PROJECT_RELATIVE_PATH;
71
72 // We add the parameter if it's defined (adapter crashes otherwise)
73 if (!nodeDebugWrapper.isNullOrUndefined(args.logCatArguments)) {
74 nodeDebugWrapper.mobilePlatformOptions.logCatArguments = [nodeDebugWrapper.parseLogCatArguments(args.logCatArguments)];
75 }
76
77 if (!nodeDebugWrapper.isNullOrUndefined(args.variant)) {
78 nodeDebugWrapper.mobilePlatformOptions.variant = args.variant;
79 }
80
81 return TelemetryHelper.generate("launch", (generator) => {
82 const resolver = new PlatformResolver();
83 return nodeDebugWrapper.remoteExtension.getPackagerPort()
84 .then(packagerPort => {
85 nodeDebugWrapper.mobilePlatformOptions.packagerPort = packagerPort;
86 const mobilePlatform = resolver.resolveMobilePlatform(args.platform, nodeDebugWrapper.mobilePlatformOptions);
87 return Q({})
88 .then(() => {
89 generator.step("checkPlatformCompatibility");
90 TargetPlatformHelper.checkTargetPlatformSupport(nodeDebugWrapper.mobilePlatformOptions.platform);
91 generator.step("startPackager");
92 return mobilePlatform.startPackager();
93 })
94 .then(() => {
95 // We've seen that if we don't prewarm the bundle cache, the app fails on the first attempt to connect to the debugger logic
96 // and the user needs to Reload JS manually. We prewarm it to prevent that issue
97 generator.step("prewarmBundleCache");
98 Log.logMessage("Prewarming bundle cache. This may take a while ...");
99 return mobilePlatform.prewarmBundleCache();
100 })
101 .then(() => {
102 generator.step("mobilePlatform.runApp");
103 Log.logMessage("Building and running application.");
104 return mobilePlatform.runApp();
105 })
106 .then(() =>
107 nodeDebugWrapper.attachRequest(this, request, args, mobilePlatform));
108 }).catch(error =>
109 nodeDebugWrapper.bailOut(this, error.message));
110 });
111 };
112 }
113
114 /**
115 * Intecept the "attachRequest" instance method of NodeDebugSession to interpret arguments
116 */
117 private customizeAttachRequest(): void {
118 const nodeDebugWrapper = this;
119 this.nodeDebugSession.prototype.attachRequest = function (request: any, args: IAttachRequestArgs) {
120 nodeDebugWrapper.requestSetup(this, args);
121 nodeDebugWrapper.attachRequest(this, request, args, new GeneralMobilePlatform(nodeDebugWrapper.mobilePlatformOptions));
122 };
123 }
124
125 /**
126 * Intecept the "disconnectRequest" instance method of NodeDebugSession to interpret arguments
127 */
128 private customizeDisconnectRequest(): void {
129 const originalRequest = this.nodeDebugSession.prototype.disconnectRequest;
130 const nodeDebugWrapper = this;
131
132 this.nodeDebugSession.prototype.disconnectRequest = function (response: any, args: any): void {
133 // First we tell the extension to stop monitoring the logcat, and then we disconnect the debugging session
134
135 if (nodeDebugWrapper.mobilePlatformOptions.platform === "android") {
136 nodeDebugWrapper.remoteExtension.stopMonitoringLogcat()
137 .catch(reason =>
138 Log.logError(`WARNING: Couldn't stop monitoring logcat: ${reason.message || reason}\n`))
139 .finally(() =>
140 originalRequest.call(this, response, args));
141 } else {
142 originalRequest.call(this, response, args);
143 }
144 };
145 }
146
147 /**
148 * Makes the required setup for request customization
149 * - Enables telemetry
150 * - Sets up mobilePlatformOptions, remote extension and projectRootPath
151 * - Starts debug server
152 * - Create global logger
153 */
154 private requestSetup(debugSession: NodeDebugSession, args: any) {
155 this.projectRootPath = this.getProjectRoot(args);
156 this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
157 this.mobilePlatformOptions = {
158 projectRoot: this.projectRootPath,
159 platform: args.platform,
160 };
161
162 // Start to send telemetry
163 this.telemetryReporter.reassignTo(new ExtensionTelemetryReporter(
164 this.appName, this.version, Telemetry.APPINSIGHTS_INSTRUMENTATIONKEY, this.projectRootPath));
165
166 // Create a server waiting for messages to re-initialize the debug session;
167 const debugServerListeningPort = this.createReinitializeServer(debugSession, args.internalDebuggerPort, args.outDir);
168 args.args = [debugServerListeningPort.toString()];
169
170 Log.SetGlobalLogger(new NodeDebugAdapterLogger(this.vscodeDebugAdapterPackage, debugSession));
171 }
172
173 /**
174 * Runs logic needed to attach.
175 * Attach should:
176 * - Enable js debugging
177 */
178 private attachRequest(debugSession: NodeDebugSession, request: any, args: any, mobilePlatform: any): Q.Promise<void> {
179 return TelemetryHelper.generate("attach", (generator) => {
180 return Q({})
181 .then(() => {
182 generator.step("mobilePlatform.enableJSDebuggingMode");
183 if (mobilePlatform) {
184 return mobilePlatform.enableJSDebuggingMode();
185 } else {
186 Log.logMessage("Debugger ready. Enable remote debugging in app.");
187 }
188 }).then(() =>
189 this.originalLaunchRequest.call(debugSession, request, args))
190 .catch(error =>
191 this.bailOut(debugSession, error.message));
192 });
193 }
194
195 /**
196 * Creates internal debug server and returns the port that the server is hook up into.
197 */
198 private createReinitializeServer(debugSession: NodeDebugSession, internalDebuggerPort: string, sourcesDir: string): number {
199 // Create the server
200 const server = http.createServer((req, res) => {
201 res.statusCode = 404;
202 if (req.url === "/refreshBreakpoints") {
203 res.statusCode = 200;
204 if (debugSession) {
205 const sourceMaps = debugSession._sourceMaps;
206 if (sourceMaps) {
207 // Flush any cached source maps
208 // Rather than cleaning internal caches we recreate
209 // SourceMaps to add downloaded bundle map to cache
210 const bundlePattern = path.join(sourcesDir, "*.bundle");
211 const sourceMaps = new this.sourceMapsConstructor(debugSession, sourcesDir, [bundlePattern]);
212 debugSession._sourceMaps = sourceMaps;
213 }
214 // Send an "initialized" event to trigger breakpoints to be re-sent
215 debugSession.sendEvent(new this.vscodeDebugAdapterPackage.InitializedEvent());
216 }
217 }
218 res.end();
219 });
220
221 // Setup listen port and on error response
222 const port = parseInt(internalDebuggerPort, 10) || 9090;
223
224 server.listen(port);
225 server.on("error", (err: Error) => {
226 TelemetryHelper.sendSimpleEvent("reinitializeServerError");
227 Log.logError("Error in debug adapter server: " + err.toString());
228 Log.logMessage("Breakpoints may not update. Consider restarting and specifying a different 'internalDebuggerPort' in launch.json");
229 });
230
231 // Return listen port
232 return port;
233 }
234
235 /**
236 * Logs error to user and finishes the debugging process.
237 */
238 private bailOut(debugSession: NodeDebugSession, message: string): void {
239 Log.logError(`Could not debug. ${message}`);
240 debugSession.sendEvent(new this.vscodeDebugAdapterPackage.TerminatedEvent());
241 process.exit(1);
242 }
243
244 /**
245 * Parses log cat arguments to a string
246 */
247 private parseLogCatArguments(userProvidedLogCatArguments: any): string {
248 return Array.isArray(userProvidedLogCatArguments)
249 ? userProvidedLogCatArguments.join(" ") // If it's an array, we join the arguments
250 : userProvidedLogCatArguments; // If not, we leave it as-is
251 }
252
253 /**
254 * Helper method to know if a value is either null or undefined
255 */
256 private isNullOrUndefined(value: any): boolean {
257 return typeof value === "undefined" || value === null;
258 }
259
260 /**
261 * Parses settings.json file for workspace root property
262 */
263 private getProjectRoot(args: any): string {
264 try {
265 let vsCodeRoot = path.resolve(args.program, "../..");
266 let settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
267 let settingsContent = fs.readFileSync(settingsPath, "utf8");
268 settingsContent = stripJsonComments(settingsContent);
269 let parsedSettings = JSON.parse(settingsContent);
270 let projectRootPath = parsedSettings["react-native-tools"].projectRoot;
271 return path.resolve(vsCodeRoot, projectRootPath);
272 } catch (e) {
273 return path.resolve(args.program, "../..");
274 }
275 }
276}
277