microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
166908e57b5023d45edd66783ac169b7f678f84f

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/extensionServer.ts

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