microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
488dfb95e5dc78ff6ac49af7223d606b8ca319d2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

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