microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.15.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/extensionServer.ts

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