microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
dbfbebd91787effa9f8df87f8fc17707a5cb28cd

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

321lines · 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 WebSocket from "ws";
7import { EventEmitter } from "events";
8import { ensurePackagerRunning } from "../common/packagerStatus";
9import {ErrorHelper} from "../common/error/errorHelper";
10import { logger } from "vscode-chrome-debug-core";
11import {ExecutionsLimiter} from "../common/executionsLimiter";
12import { FileSystem as NodeFileSystem} from "../common/node/fileSystem";
13import { ForkedAppWorker } from "./forkedAppWorker";
14import { ScriptImporter } from "./scriptImporter";
15import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
16import * as nls from "vscode-nls";
17import { InternalErrorCode } from "../common/error/internalErrorCode";
18const localize = nls.loadMessageBundle();
19
20export interface RNAppMessage {
21 method: string;
22 url?: string;
23 // These objects have also other properties but that we don't currently use
24}
25
26export interface IDebuggeeWorker {
27 start(): Q.Promise<any>;
28 stop(): void;
29 postMessage(message: RNAppMessage): void;
30}
31
32function printDebuggingError(error: Error, reason: any) {
33 const nestedError = ErrorHelper.getNestedError(error, InternalErrorCode.DebuggingWontWorkReloadJSAndReconnect, reason);
34
35 logger.error(nestedError.message);
36}
37
38 /** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
39 * and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
40 * is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
41 * When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
42 */
43
44export class MultipleLifetimesAppWorker extends EventEmitter {
45 public static WORKER_BOOTSTRAP = `
46// Initialize some variables before react-native code would access them
47var onmessage=null, self=global;
48// Cache Node's original require as __debug__.require
49global.__debug__={require: require};
50// avoid Node's GLOBAL deprecation warning
51Object.defineProperty(global, "GLOBAL", {
52 configurable: true,
53 writable: true,
54 enumerable: true,
55 value: global
56});
57// Prevent leaking process.versions from debugger process to
58// worker because pure React Native doesn't do that and some packages as js-md5 rely on this behavior
59Object.defineProperty(process, "versions", {
60 value: undefined
61});
62
63var vscodeHandlers = {
64 'vscode_reloadApp': function () {
65 try {
66 global.require('NativeModules').DevMenu.reload();
67 } catch (err) {
68 // ignore
69 }
70 },
71 'vscode_showDevMenu': function () {
72 try {
73 var DevMenu = global.require('NativeModules').DevMenu.show();
74 } catch (err) {
75 // ignore
76 }
77 }
78};
79
80process.on("message", function (message) {
81 if (message.data && vscodeHandlers[message.data.method]) {
82 vscodeHandlers[message.data.method]();
83 } else if(onmessage) {
84 onmessage(message);
85 }
86});
87
88var postMessage = function(message){
89 process.send(message);
90};
91
92if (!self.postMessage) {
93 self.postMessage = postMessage;
94}
95
96var importScripts = (function(){
97 var fs=require('fs'), vm=require('vm');
98 return function(scriptUrl){
99 var scriptCode = fs.readFileSync(scriptUrl, "utf8");
100 vm.runInThisContext(scriptCode, {filename: scriptUrl});
101 };
102})();`;
103
104 public static WORKER_DONE = `// Notify debugger that we're done with loading
105// and started listening for IPC messages
106postMessage({workerLoaded:true});`;
107
108 public static FETCH_STUB = `(function(self) {
109 'use strict';
110
111 if (self.fetch) {
112 return
113 }
114
115 self.fetch = fetch;
116
117 function fetch(url) {
118 return new Promise((resolve, reject) => {
119 var data = require("fs").readFileSync(url, 'utf8');
120 resolve(
121 {
122 text: function () {
123 return data;
124 }
125 });
126 });
127 }
128 })(global);`;
129
130 private packagerAddress: string;
131 private packagerPort: number;
132 private sourcesStoragePath: string;
133 private projectRootPath: string;
134 private packagerRemoteRoot?: string;
135 private packagerLocalRoot?: string;
136 private debuggerWorkerUrlPath?: string;
137 private socketToApp: WebSocket;
138 private singleLifetimeWorker: IDebuggeeWorker | null;
139 private webSocketConstructor: (url: string) => WebSocket;
140
141 private executionLimiter = new ExecutionsLimiter();
142 private nodeFileSystem = new NodeFileSystem();
143 private scriptImporter: ScriptImporter;
144
145 constructor(
146 attachRequestArguments: any,
147 sourcesStoragePath: string,
148 projectRootPath: string,
149 {
150 webSocketConstructor = (url: string) => new WebSocket(url),
151 } = {}) {
152 super();
153 this.packagerAddress = attachRequestArguments.address || "localhost";
154 this.packagerPort = attachRequestArguments.port;
155 this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
156 this.packagerLocalRoot = attachRequestArguments.localRoot;
157 this.debuggerWorkerUrlPath = attachRequestArguments.debuggerWorkerUrlPath;
158 this.sourcesStoragePath = sourcesStoragePath;
159 this.projectRootPath = projectRootPath;
160 if (!this.sourcesStoragePath)
161 throw ErrorHelper.getInternalError(InternalErrorCode.SourcesStoragePathIsNullOrEmpty);
162 this.webSocketConstructor = webSocketConstructor;
163 this.scriptImporter = new ScriptImporter(this.packagerAddress, this.packagerPort, sourcesStoragePath, this.packagerRemoteRoot, this.packagerLocalRoot);
164 }
165
166 public start(retryAttempt: boolean = false): Q.Promise<any> {
167 const errPackagerNotRunning = ErrorHelper.getInternalError(InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort, this.packagerPort);
168
169 return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
170 .then(() => {
171 // Don't fetch debugger worker on socket disconnect
172 return retryAttempt ? Q.resolve<void>(void 0) :
173 this.downloadAndPatchDebuggerWorker();
174 })
175 .then(() => this.createSocketToApp(retryAttempt));
176 }
177
178 public stop() {
179 if (this.socketToApp) {
180 this.socketToApp.removeAllListeners();
181 this.socketToApp.close();
182 }
183
184 if (this.singleLifetimeWorker) {
185 this.singleLifetimeWorker.stop();
186 }
187 }
188
189 public downloadAndPatchDebuggerWorker(): Q.Promise<void> {
190 let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
191 return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath, this.projectRootPath, this.debuggerWorkerUrlPath)
192 .then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
193 .then((workerContent: string) => {
194 const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
195 // Add our customizations to debugger worker to get it working smoothly
196 // in Node env and polyfill WebWorkers API over Node's IPC.
197 const modifiedDebuggeeContent = [
198 MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
199 isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
200 workerContent,
201 MultipleLifetimesAppWorker.WORKER_DONE,
202 ].join("\n");
203 return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
204 });
205 }
206
207 private startNewWorkerLifetime(): Q.Promise<void> {
208 this.singleLifetimeWorker = new ForkedAppWorker(this.packagerAddress, this.packagerPort, this.sourcesStoragePath, this.projectRootPath,
209 (message) => {
210 this.sendMessageToApp(message);
211 },
212 this.packagerRemoteRoot, this.packagerLocalRoot);
213 logger.verbose("A new app worker lifetime was created.");
214 return this.singleLifetimeWorker.start()
215 .then(startedEvent => {
216 this.emit("connected", startedEvent);
217 });
218 }
219
220 private createSocketToApp(retryAttempt: boolean = false): Q.Promise<void> {
221 let deferred = Q.defer<void>();
222 this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
223 this.socketToApp.on("open", () => {
224 this.onSocketOpened();
225 });
226 this.socketToApp.on("close",
227 () => {
228 this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
229 /*
230 * It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
231 * it closes the socket because it already has a connection to a debugger.
232 * https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
233 */
234 let msgKey = "_closeMessage";
235 if (this.socketToApp[msgKey] === "Another debugger is already connected") {
236 deferred.reject(ErrorHelper.getInternalError(InternalErrorCode.AnotherDebuggerConnectedToPackager));
237 }
238 logger.log(localize("DisconnectedFromThePackagerToReactNative", "Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon..."));
239 });
240 setTimeout(() => {
241 this.start(true /* retryAttempt */);
242 }, 100);
243 });
244 this.socketToApp.on("message",
245 (message: any) => this.onMessage(message));
246 this.socketToApp.on("error",
247 (error: Error) => {
248 if (retryAttempt) {
249 printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.ReconnectionToPackagerFailedCheckForErrorsOrRestartReactNative), error);
250 }
251
252 deferred.reject(error);
253 });
254
255 // In an attempt to catch failures in starting the packager on first attempt,
256 // wait for 300 ms before resolving the promise
257 Q.delay(300).done(() => deferred.resolve(void 0));
258 return deferred.promise;
259 }
260
261 private debuggerProxyUrl() {
262 return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
263 }
264
265 private onSocketOpened() {
266 this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
267 logger.log(localize("EstablishedConnectionWithPackagerToReactNativeApp", "Established a connection with the Proxy (Packager) to the React Native application")));
268 }
269
270 private killWorker() {
271 if (!this.singleLifetimeWorker) return;
272 this.singleLifetimeWorker.stop();
273 this.singleLifetimeWorker = null;
274 }
275
276 private onMessage(message: string) {
277 try {
278 logger.verbose("From RN APP: " + message);
279 let object = <RNAppMessage>JSON.parse(message);
280 if (object.method === "prepareJSRuntime") {
281 // In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
282 // when user reloads an app, hence we need to try to kill it here either.
283 this.killWorker();
284 // The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
285 this.gotPrepareJSRuntime(object);
286 } else if (object.method === "$disconnected") {
287 // We need to shutdown the current app worker, and create a new lifetime
288 this.killWorker();
289 } else if (object.method) {
290 // All the other messages are handled by the single lifetime worker
291 if (this.singleLifetimeWorker) {
292 this.singleLifetimeWorker.postMessage(object);
293 }
294 } else {
295 // Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
296 logger.verbose(`The react-native app sent a message without specifying a method: ${message}`);
297 }
298 } catch (exception) {
299 printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToProcessMessageFromReactNativeApp, message), exception);
300 }
301 }
302
303 private gotPrepareJSRuntime(message: any): void {
304 // Create the sandbox, and replay that we finished processing the message
305 this.startNewWorkerLifetime().done(() => {
306 this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
307 }, error => printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToPrepareJSRuntimeEnvironment, message), error));
308 }
309
310 private sendMessageToApp(message: any): void {
311 let stringified: string = "";
312 try {
313 stringified = JSON.stringify(message);
314 logger.verbose(`To RN APP: ${stringified}`);
315 this.socketToApp.send(stringified);
316 } catch (exception) {
317 let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
318 printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToSendMessageToTheReactNativeApp, messageToShow), exception);
319 }
320 }
321}