microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8a67e140aee58da792b1e9c1c17358a98d5a3704

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

289lines · 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 { PlatformResolver } from "./platformResolver";
13import { IRunOptions } from "../common/launchArgs";
14import { TargetPlatformHelper } from "../common/targetPlatformHelper";
15import { ExtensionTelemetryReporter, ReassignableTelemetryReporter } from "../common/telemetryReporters";
16import { NodeDebugAdapterLogger } from "../common/log/loggers";
17import { Log } from "../common/log/log";
18import { LogLevel } from "../common/log/logHelper";
19import { GeneralMobilePlatform } from "../common/generalMobilePlatform";
20
21import { MultipleLifetimesAppWorker } from "./appWorker";
22
23export function makeSession(
24 debugSessionClass: typeof ChromeDebuggerCorePackage.ChromeDebugSession,
25 debugSessionOpts: ChromeDebuggerCorePackage.IChromeDebugSessionOpts,
26 debugAdapterPackage: typeof VSCodeDebugAdapterPackage,
27 telemetryReporter: ReassignableTelemetryReporter,
28 appName: string, version: string): typeof ChromeDebuggerCorePackage.ChromeDebugSession {
29
30 return class extends debugSessionClass {
31
32 private projectRootPath: string;
33 private remoteExtension: RemoteExtension;
34 private mobilePlatformOptions: IRunOptions;
35 private appWorker: MultipleLifetimesAppWorker | null = null;
36
37 constructor(debuggerLinesAndColumnsStartAt1?: boolean, isServer?: boolean) {
38 super(debuggerLinesAndColumnsStartAt1, isServer, debugSessionOpts);
39 }
40
41 // Override ChromeDebugSession's sendEvent to control what we will send to client
42 public sendEvent(event: VSCodeDebugAdapterPackage.Event): void {
43 // Do not send "terminated" events signaling about session's restart to client as it would cause it
44 // to restart adapter's process, while we want to stay alive and don't want to interrupt connection
45 // to packager.
46
47 if (event.event === "terminated" && event.body && event.body.restart) {
48
49 // Worker has been reloaded and switched to "continue" state
50 // So we have to send "continued" event to client instead of "terminated"
51 // Otherwise client might mistakenly show "stopped" state
52 let continuedEvent: VSCodeDebugAdapterPackage.ContinuedEvent = {
53 event: "continued",
54 type: "event",
55 seq: event["seq"], // tslint:disable-line
56 body: { threadId: event.body.threadId },
57 };
58
59 super.sendEvent(continuedEvent);
60 return;
61 }
62
63 super.sendEvent(event);
64 }
65
66 protected dispatchRequest(request: VSCodeDebugAdapterPackage.Request): void {
67 if (request.command === "disconnect")
68 return this.disconnect(request);
69
70 if (request.command === "attach")
71 return this.attach(request);
72
73 if (request.command === "launch")
74 return this.launch(request);
75
76 return super.dispatchRequest(request);
77 }
78
79 private launch(request: VSCodeDebugAdapterPackage.Request): void {
80 this.requestSetup(request.arguments)
81 .then(() => {
82 // We add the parameter if it's defined (adapter crashes otherwise)
83 if (!isNullOrUndefined(request.arguments.logCatArguments)) {
84 this.mobilePlatformOptions.logCatArguments = [parseLogCatArguments(request.arguments.logCatArguments)];
85 }
86
87 if (!isNullOrUndefined(request.arguments.variant)) {
88 this.mobilePlatformOptions.variant = request.arguments.variant;
89 }
90
91 if (!isNullOrUndefined(request.arguments.scheme)) {
92 this.mobilePlatformOptions.scheme = request.arguments.scheme;
93 }
94
95 TelemetryHelper.generate("launch", (generator) => {
96 return this.remoteExtension.getPackagerPort()
97 .then((packagerPort: number) => {
98 this.mobilePlatformOptions.packagerPort = packagerPort;
99 const mobilePlatform = new PlatformResolver()
100 .resolveMobilePlatform(request.arguments.platform, this.mobilePlatformOptions);
101
102 generator.step("checkPlatformCompatibility");
103 TargetPlatformHelper.checkTargetPlatformSupport(this.mobilePlatformOptions.platform);
104 generator.step("startPackager");
105 return mobilePlatform.startPackager()
106 .then(() => {
107 // 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
108 // and the user needs to Reload JS manually. We prewarm it to prevent that issue
109 generator.step("prewarmBundleCache");
110 Log.logMessage("Prewarming bundle cache. This may take a while ...");
111 return mobilePlatform.prewarmBundleCache();
112 })
113 .then(() => {
114 generator.step("mobilePlatform.runApp");
115 Log.logMessage("Building and running application.");
116 return mobilePlatform.runApp();
117 })
118 .then(() => {
119 return this.attachRequest(request, packagerPort, mobilePlatform);
120 });
121 })
122 .catch(error => this.bailOut(error.message));
123 });
124 });
125 }
126
127 private attach(request: VSCodeDebugAdapterPackage.Request): void {
128 this.requestSetup(request.arguments)
129 .then(() => {
130 this.remoteExtension.getPackagerPort()
131 .then((packagerPort: number) => this.attachRequest(request, packagerPort));
132 });
133 }
134
135 private disconnect(request: VSCodeDebugAdapterPackage.Request): void {
136 // The client is about to disconnect so first we need to stop app worker
137 if (this.appWorker) {
138 this.appWorker.stop();
139 }
140
141 // Then we tell the extension to stop monitoring the logcat, and then we disconnect the debugging session
142 if (this.mobilePlatformOptions.platform === "android") {
143 this.remoteExtension.stopMonitoringLogcat()
144 .catch(reason => Log.logError(`WARNING: Couldn't stop monitoring logcat: ${reason.message || reason}\n`))
145 .finally(() => super.dispatchRequest(request));
146 } else {
147 super.dispatchRequest(request);
148 }
149 }
150
151 private requestSetup(args: any): Q.Promise<void> {
152 this.projectRootPath = getProjectRoot(args);
153 this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
154 this.mobilePlatformOptions = {
155 projectRoot: this.projectRootPath,
156 platform: args.platform,
157 targetType: args.targetType || "simulator",
158 };
159
160 // Start to send telemetry
161 telemetryReporter.reassignTo(new ExtensionTelemetryReporter(
162 appName, version, Telemetry.APPINSIGHTS_INSTRUMENTATIONKEY, this.projectRootPath));
163
164 Log.SetGlobalLogger(new NodeDebugAdapterLogger(debugAdapterPackage, this));
165
166 if (args.target) {
167 this.mobilePlatformOptions.target = args.target;
168 return Q.resolve(void 0);
169 } else {
170 return this.remoteExtension.getApplicationTarget(args.platform, args.targetType)
171 .then(target => {
172 this.mobilePlatformOptions.target = target;
173 });
174 }
175 }
176
177 /**
178 * Runs logic needed to attach.
179 * Attach should:
180 * - Enable js debugging
181 */
182 private attachRequest(
183 request: VSCodeDebugAdapterPackage.Request,
184 packagerPort: number,
185 mobilePlatform?: GeneralMobilePlatform): Q.Promise<void> {
186 return TelemetryHelper.generate("attach", (generator) => {
187 return Q({})
188 .then(() => {
189 generator.step("mobilePlatform.enableJSDebuggingMode");
190 if (mobilePlatform) {
191 return mobilePlatform.enableJSDebuggingMode();
192 } else {
193 Log.logMessage("Debugger ready. Enable remote debugging in app.");
194 return void 0;
195 }
196 })
197 .then(() => {
198
199 Log.logMessage("Starting debugger app worker.");
200 // TODO: remove dependency on args.program - "program" property is technically
201 // no more required in launch configuration and could be removed
202 const workspaceRootPath = path.resolve(path.dirname(request.arguments.program), "..");
203 const sourcesStoragePath = path.join(workspaceRootPath, ".vscode", ".react");
204
205 // If launch is invoked first time, appWorker is undefined, so create it here
206 this.appWorker = new MultipleLifetimesAppWorker(packagerPort, sourcesStoragePath);
207 this.appWorker.on("connected", (port: number) => {
208 Log.logMessage("Debugger worker loaded runtime on port " + port);
209 // Don't mutate original request to avoid side effects
210 let attachArguments = Object.assign({}, request.arguments, { port, restart: true, request: "attach" });
211 let attachRequest = Object.assign({}, request, { command: "attach", arguments: attachArguments });
212
213 // Reinstantiate debug adapter, as the current implementation of ChromeDebugAdapter
214 // doesn't allow us to reattach to another debug target easily. As of now it's easier
215 // to throw previous instance out and create a new one.
216 this._debugAdapter = new (<any>debugSessionOpts.adapter)(debugSessionOpts, this);
217 super.dispatchRequest(attachRequest);
218 });
219
220 return this.appWorker.start();
221 })
222 .catch(error => this.bailOut(error.message));
223 });
224 }
225
226 /**
227 * Logs error to user and finishes the debugging process.
228 */
229 private bailOut(message: string): void {
230 Log.logError(`Could not debug. ${message}`);
231 this.sendEvent(new debugAdapterPackage.TerminatedEvent());
232 }
233 };
234}
235
236export function makeAdapter(debugAdapterClass: typeof Node2DebugAdapterPackage.Node2DebugAdapter): typeof Node2DebugAdapterPackage.Node2DebugAdapter {
237 return class extends debugAdapterClass {
238 public doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
239 // We need to overwrite ChromeDebug's _attachMode to let Node2 adapter
240 // to set up breakpoints on initial pause event
241 this._attachMode = false;
242 return super.doAttach(port, targetUrl, address, timeout);
243 }
244
245 public setBreakpoints(args: any, requestSeq: number, ids?: number[]): Promise<Node2DebugAdapterPackage.ISetBreakpointsResponseBody> {
246 // We need to overwrite ChromeDebug's setBreakpoints to get rid unhandled rejections
247 // when breakpoints are being set up unsuccessfully
248 return super.setBreakpoints(args, requestSeq, ids).catch((err) => {
249 Log.logInternalMessage(LogLevel.Error, err.message);
250 return {
251 breakpoints: [],
252 };
253 });
254 }
255 };
256}
257
258/**
259 * Parses log cat arguments to a string
260 */
261function parseLogCatArguments(userProvidedLogCatArguments: any): string {
262 return Array.isArray(userProvidedLogCatArguments)
263 ? userProvidedLogCatArguments.join(" ") // If it's an array, we join the arguments
264 : userProvidedLogCatArguments; // If not, we leave it as-is
265}
266
267/**
268 * Helper method to know if a value is either null or undefined
269 */
270function isNullOrUndefined(value: any): boolean {
271 return typeof value === "undefined" || value === null;
272}
273
274/**
275 * Parses settings.json file for workspace root property
276 */
277function getProjectRoot(args: any): string {
278 try {
279 let vsCodeRoot = path.resolve(args.program, "../..");
280 let settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
281 let settingsContent = fs.readFileSync(settingsPath, "utf8");
282 settingsContent = stripJsonComments(settingsContent);
283 let parsedSettings = JSON.parse(settingsContent);
284 let projectRootPath = parsedSettings["react-native-tools"].projectRoot;
285 return path.resolve(vsCodeRoot, projectRootPath);
286 } catch (e) {
287 return path.resolve(args.program, "../..");
288 }
289}
290