microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2a2a4f72fa411361cbf3aabf879beeca349cd868

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/extensionServer.ts

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