microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
10bc5320aeda86f9f9aee59bb732e61314aaad7e

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

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