microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e67ace8aab1da5ee01f5a9eec52e406501d301e4

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/extensionServer.ts

314lines · 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 vscode from "vscode";
6
7import {MessagingHelper}from "../common/extensionMessaging";
8import {OutputChannelLogger} from "./log/OutputChannelLogger";
9import {Packager} from "../common/packager";
10import {LogCatMonitor} from "./android/logCatMonitor";
11import {FileSystem} from "../common/node/fileSystem";
12import {SettingsHelper} from "./settingsHelper";
13import {Telemetry} from "../common/telemetry";
14import {PlatformResolver} from "./platformResolver";
15import {TelemetryHelper} from "../common/telemetryHelper";
16import {TargetPlatformHelper} from "../common/targetPlatformHelper";
17import {MobilePlatformDeps} from "./generalMobilePlatform";
18import {IRemoteExtension, OpenFileRequest} from "../common/remoteExtension";
19import * as rpc from "noice-json-rpc";
20import * as WebSocket from "ws";
21import WebSocketServer = WebSocket.Server;
22import * as nls from "vscode-nls";
23const localize = nls.loadMessageBundle();
24
25export class ExtensionServer implements vscode.Disposable {
26 public api: IRemoteExtension;
27 public isDisposed: boolean = false;
28 private serverInstance: WebSocketServer | null;
29 private reactNativePackager: Packager;
30 private pipePath: string;
31 private logCatMonitor: LogCatMonitor | null = null;
32 private logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
33
34 public constructor(projectRootPath: string, reactNativePackager: Packager) {
35 this.pipePath = MessagingHelper.getPath(projectRootPath);
36 this.reactNativePackager = reactNativePackager;
37 }
38
39 /**
40 * Starts the server.
41 */
42 public setup(): Q.Promise<void> {
43 this.isDisposed = false;
44
45 return Q.Promise((resolve, reject) => {
46 this._setup(resolve, reject);
47 });
48 }
49
50 /**
51 * Stops the server.
52 */
53 public dispose(): void {
54 this.isDisposed = true;
55 if (this.serverInstance) {
56 this.serverInstance.close();
57 this.serverInstance = null;
58 }
59
60 this.reactNativePackager.statusIndicator.dispose();
61 this.reactNativePackager.stop(true);
62 this.stopMonitoringLogCat();
63 }
64
65 private _setup(resolve: (val: void | Q.IPromise<void>) => void, reject: (reason: any) => void): void {
66 const errorCallback = this.recoverServer.bind(this, resolve, reject);
67 let launchCallback = (done: (val: void | Q.IPromise<void>) => void) => {
68 this.logger.debug(`Extension messaging server started at ${this.pipePath}.`);
69
70 if (this.serverInstance) {
71 this.serverInstance.removeListener("error", errorCallback);
72 this.serverInstance.on("error", this.recoverServer.bind(this, null, null));
73 }
74 done(void 0);
75 };
76
77 this.serverInstance = new WebSocketServer({port: <any>this.pipePath});
78 this.api = new rpc.Server(this.serverInstance).api();
79 this.serverInstance.on("listening", launchCallback.bind(this, resolve));
80 this.serverInstance.on("error", errorCallback);
81
82 this.setupApiHandlers();
83 }
84
85 private setupApiHandlers(): void {
86 let methods: any = {};
87 methods.stopMonitoringLogCat = this.stopMonitoringLogCat.bind(this);
88 methods.getPackagerPort = this.getPackagerPort.bind(this);
89 methods.sendTelemetry = this.sendTelemetry.bind(this);
90 methods.openFileAtLocation = this.openFileAtLocation.bind(this);
91 methods.showInformationMessage = this.showInformationMessage.bind(this);
92 methods.launch = this.launch.bind(this);
93 methods.showDevMenu = this.showDevMenu.bind(this);
94 methods.reloadApp = this.reloadApp.bind(this);
95
96 this.api.Extension.expose(methods);
97 }
98
99 private showDevMenu(deviceId?: string) {
100 this.api.Debugger.emitShowDevMenu(deviceId);
101 }
102
103 private reloadApp(deviceId?: string) {
104 this.api.Debugger.emitReloadApp(deviceId);
105 }
106
107 /**
108 * Recovers the server in case the named socket we use already exists, but no other instance of VSCode is active.
109 */
110 private recoverServer(resolve: (value: void) => {} , reject: (reason: any) => {}, error: any): void {
111 let errorHandler = (e: any) => {
112 /* The named socket is not used. */
113 if (e.code === "ECONNREFUSED") {
114 new FileSystem().removePathRecursivelyAsync(this.pipePath)
115 .then(() => {
116 if (resolve && reject) {
117 return this._setup(resolve, reject);
118 } else {
119 return this.setup();
120 }
121 })
122 .done();
123 }
124 };
125
126 /* The named socket already exists. */
127 if (error.code === "EADDRINUSE") {
128 let clientSocket = new WebSocket(`ws+unix://${this.pipePath}`);
129 clientSocket.on("error", errorHandler);
130 clientSocket.on("open", function() {
131 clientSocket.close();
132 });
133 }
134 }
135
136 /**
137 * Message handler for GET_PACKAGER_PORT.
138 */
139 private getPackagerPort(projectFolder: string): number {
140 return SettingsHelper.getPackagerPort(projectFolder);
141 }
142
143 /**
144 * Message handler for OPEN_FILE_AT_LOCATION
145 */
146 private openFileAtLocation(openFileRequest: OpenFileRequest): Promise<void> {
147 const { filename, lineNumber } = openFileRequest;
148 return new Promise((resolve) => {
149 vscode.workspace.openTextDocument(vscode.Uri.file(filename))
150 .then((document: vscode.TextDocument) => {
151 vscode.window.showTextDocument(document)
152 .then((editor: vscode.TextEditor) => {
153 let range = editor.document.lineAt(lineNumber - 1).range;
154 editor.selection = new vscode.Selection(range.start, range.end);
155 editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
156 resolve();
157 });
158 });
159 });
160 }
161
162 private stopMonitoringLogCat(): void {
163 if (this.logCatMonitor) {
164 this.logCatMonitor.dispose();
165 this.logCatMonitor = null;
166 }
167 }
168
169 /**
170 * Sends telemetry
171 */
172 private sendTelemetry(telemetryRequest: Telemetry.TelemetryRequest): void {
173 const { extensionId, extensionVersion, appInsightsKey, eventName, properties, measures } = telemetryRequest;
174 Telemetry.sendExtensionTelemetry(extensionId, extensionVersion, appInsightsKey, eventName, properties, measures);
175 }
176
177 /**
178 * Message handler for SHOW_INFORMATION_MESSAGE
179 */
180 private showInformationMessage(message: string): void {
181 vscode.window.showInformationMessage(message);
182 }
183
184 private launch(request: any): Promise<any> {
185 let mobilePlatformOptions = requestSetup(request.arguments);
186
187 // We add the parameter if it's defined (adapter crashes otherwise)
188 if (!isNullOrUndefined(request.arguments.logCatArguments)) {
189 mobilePlatformOptions.logCatArguments = [parseLogCatArguments(request.arguments.logCatArguments)];
190 }
191
192 if (!isNullOrUndefined(request.arguments.variant)) {
193 mobilePlatformOptions.variant = request.arguments.variant;
194 }
195
196 if (!isNullOrUndefined(request.arguments.scheme)) {
197 mobilePlatformOptions.scheme = request.arguments.scheme;
198 }
199
200 if (!isNullOrUndefined(request.arguments.productName)) {
201 mobilePlatformOptions.productName = request.arguments.productName;
202 }
203
204 if (!isNullOrUndefined(request.arguments.launchActivity)) {
205 mobilePlatformOptions.debugLaunchActivity = request.arguments.launchActivity;
206 }
207
208 if (request.arguments.type === "reactnativedirect") {
209 mobilePlatformOptions.isDirect = true;
210 }
211
212 mobilePlatformOptions.packagerPort = SettingsHelper.getPackagerPort(request.arguments.cwd || request.arguments.program);
213 const platformDeps: MobilePlatformDeps = {
214 packager: this.reactNativePackager,
215 };
216 const mobilePlatform = new PlatformResolver()
217 .resolveMobilePlatform(request.arguments.platform, mobilePlatformOptions, platformDeps);
218 return new Promise((resolve, reject) => {
219 let extProps: any = {
220 platform: {
221 value: request.arguments.platform,
222 isPii: false,
223 },
224 };
225
226 if (mobilePlatformOptions.isDirect) {
227 extProps.isDirect = {
228 value: true,
229 isPii: false,
230 };
231 }
232
233 TelemetryHelper.generate("launch", extProps, (generator) => {
234 generator.step("checkPlatformCompatibility");
235 TargetPlatformHelper.checkTargetPlatformSupport(mobilePlatformOptions.platform);
236 return mobilePlatform.beforeStartPackager()
237 .then(() => {
238 generator.step("startPackager");
239 return mobilePlatform.startPackager();
240 })
241 .then(() => {
242 // 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
243 // and the user needs to Reload JS manually. We prewarm it to prevent that issue
244 generator.step("prewarmBundleCache");
245 this.logger.info(localize("PrewarmingBundleCache", "Prewarming bundle cache. This may take a while ..."));
246 return mobilePlatform.prewarmBundleCache();
247 })
248 .then(() => {
249 generator.step("mobilePlatform.runApp").add("target", mobilePlatformOptions.target, false);
250 this.logger.info(localize("BuildingAndRunningApplication", "Building and running application."));
251 return mobilePlatform.runApp();
252 })
253 .then(() => {
254 if (mobilePlatformOptions.isDirect) {
255 generator.step("mobilePlatform.enableDirectDebuggingMode");
256 if (request.arguments.platform === "android") {
257 this.logger.info(localize("PrepareHermesDebugging", "Prepare Hermes debugging (experimental)"));
258 }
259 return mobilePlatform.disableJSDebuggingMode();
260 }
261 generator.step("mobilePlatform.enableJSDebuggingMode");
262 this.logger.info(localize("EnableJSDebugging", "Enable JS Debugging"));
263 return mobilePlatform.enableJSDebuggingMode();
264 })
265 .then(() => {
266 resolve();
267 })
268 .catch(error => {
269 this.logger.error(error);
270 reject(error);
271 });
272 });
273 });
274 }
275}
276
277/**
278 * Parses log cat arguments to a string
279 */
280function parseLogCatArguments(userProvidedLogCatArguments: any): string {
281 return Array.isArray(userProvidedLogCatArguments)
282 ? userProvidedLogCatArguments.join(" ") // If it's an array, we join the arguments
283 : userProvidedLogCatArguments; // If not, we leave it as-is
284}
285
286function isNullOrUndefined(value: any): boolean {
287 return typeof value === "undefined" || value === null;
288}
289
290function requestSetup(args: any): any {
291 const workspaceFolder: vscode.WorkspaceFolder = <vscode.WorkspaceFolder>vscode.workspace.getWorkspaceFolder(vscode.Uri.file(args.cwd || args.program));
292 const projectRootPath = getProjectRoot(args);
293 let mobilePlatformOptions: any = {
294 workspaceRoot: workspaceFolder.uri.fsPath,
295 projectRoot: projectRootPath,
296 platform: args.platform,
297 env: args.env,
298 envFile: args.envFile,
299 target: args.target || "simulator",
300 };
301
302 if (!args.runArguments) {
303 let runArgs = SettingsHelper.getRunArgs(args.platform, args.target || "simulator", workspaceFolder.uri);
304 mobilePlatformOptions.runArguments = runArgs;
305 } else {
306 mobilePlatformOptions.runArguments = args.runArguments;
307 }
308
309 return mobilePlatformOptions;
310}
311
312function getProjectRoot(args: any): string {
313 return SettingsHelper.getReactNativeProjectRoot(args.cwd || args.program);
314}
315