microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
d64a6928cc37c35f73a48f620835696450994d2e

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

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