microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.11.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

372lines · 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
63function getNativeModules() {
64 var NativeModules;
65 try {
66 // This approach is for old RN versions
67 NativeModules = global.require('NativeModules');
68 } catch (err) {
69 // ignore error and try another way for more recent RN versions
70 try {
71 var nativeModuleId;
72 var modules = global.__r.getModules();
73 var ids = Object.keys(modules);
74 for (var i = 0; i < ids.length; i++) {
75 if (modules[ids[i]].verboseName) {
76 var packagePath = new String(modules[ids[i]].verboseName);
77 if (packagePath.indexOf("react-native/Libraries/BatchedBridge/NativeModules.js") > 0) {
78 nativeModuleId = parseInt(ids[i], 10);
79 break;
80 }
81 }
82 }
83 if (nativeModuleId) {
84 NativeModules = global.__r(nativeModuleId);
85 }
86 }
87 catch (err) {
88 // suppress errors
89 }
90 }
91 return NativeModules;
92}
93
94// Originally, this was made for iOS only
95var vscodeHandlers = {
96 'vscode_reloadApp': function () {
97 var NativeModules = getNativeModules();
98 if (NativeModules) {
99 NativeModules.DevMenu.reload();
100 }
101 },
102 'vscode_showDevMenu': function () {
103 var NativeModules = getNativeModules();
104 if (NativeModules) {
105 NativeModules.DevMenu.show();
106 }
107 }
108};
109
110process.on("message", function (message) {
111 if (message.data && vscodeHandlers[message.data.method]) {
112 vscodeHandlers[message.data.method]();
113 } else if(onmessage) {
114 onmessage(message);
115 }
116});
117
118var postMessage = function(message){
119 process.send(message);
120};
121
122if (!self.postMessage) {
123 self.postMessage = postMessage;
124}
125
126var importScripts = (function(){
127 var fs=require('fs'), vm=require('vm');
128 return function(scriptUrl){
129 var scriptCode = fs.readFileSync(scriptUrl, "utf8");
130 vm.runInThisContext(scriptCode, {filename: scriptUrl});
131 };
132})();`;
133
134 public static CONSOLE_TRACE_PATCH = `// Worker is ran as nodejs process, so console.trace() writes to stderr and it leads to error in native app
135// To avoid this console.trace() is overridden to print stacktrace via console.log()
136// Please, see Node JS implementation: https://github.com/nodejs/node/blob/master/lib/internal/console/constructor.js
137console.trace = (function() {
138 return function() {
139 try {
140 var err = {
141 name: 'Trace',
142 message: require('util').format.apply(null, arguments)
143 };
144 // Node uses 10, but usually it's not enough for RN app trace
145 Error.stackTraceLimit = 30;
146 Error.captureStackTrace(err, console.trace);
147 console.log(err.stack);
148 } catch (e) {
149 console.error(e);
150 }
151 };
152})();`;
153
154 public static WORKER_DONE = `// Notify debugger that we're done with loading
155// and started listening for IPC messages
156postMessage({workerLoaded:true});`;
157
158 public static FETCH_STUB = `(function(self) {
159 'use strict';
160
161 if (self.fetch) {
162 return
163 }
164
165 self.fetch = fetch;
166
167 function fetch(url) {
168 return new Promise((resolve, reject) => {
169 var data = require("fs").readFileSync(url, 'utf8');
170 resolve(
171 {
172 text: function () {
173 return data;
174 }
175 });
176 });
177 }
178 })(global);`;
179
180 private packagerAddress: string;
181 private packagerPort: number;
182 private sourcesStoragePath: string;
183 private projectRootPath: string;
184 private packagerRemoteRoot?: string;
185 private packagerLocalRoot?: string;
186 private debuggerWorkerUrlPath?: string;
187 private socketToApp: WebSocket;
188 private singleLifetimeWorker: IDebuggeeWorker | null;
189 private webSocketConstructor: (url: string) => WebSocket;
190
191 private executionLimiter = new ExecutionsLimiter();
192 private nodeFileSystem = new NodeFileSystem();
193 private scriptImporter: ScriptImporter;
194
195 constructor(
196 attachRequestArguments: any,
197 sourcesStoragePath: string,
198 projectRootPath: string,
199 {
200 webSocketConstructor = (url: string) => new WebSocket(url),
201 } = {}) {
202 super();
203 this.packagerAddress = attachRequestArguments.address || "localhost";
204 this.packagerPort = attachRequestArguments.port;
205 this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
206 this.packagerLocalRoot = attachRequestArguments.localRoot;
207 this.debuggerWorkerUrlPath = attachRequestArguments.debuggerWorkerUrlPath;
208 this.sourcesStoragePath = sourcesStoragePath;
209 this.projectRootPath = projectRootPath;
210 if (!this.sourcesStoragePath)
211 throw ErrorHelper.getInternalError(InternalErrorCode.SourcesStoragePathIsNullOrEmpty);
212 this.webSocketConstructor = webSocketConstructor;
213 this.scriptImporter = new ScriptImporter(this.packagerAddress, this.packagerPort, sourcesStoragePath, this.packagerRemoteRoot, this.packagerLocalRoot);
214 }
215
216 public start(retryAttempt: boolean = false): Q.Promise<any> {
217 const errPackagerNotRunning = ErrorHelper.getInternalError(InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort, this.packagerPort);
218
219 return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
220 .then(() => {
221 // Don't fetch debugger worker on socket disconnect
222 return retryAttempt ? Q.resolve<void>(void 0) :
223 this.downloadAndPatchDebuggerWorker();
224 })
225 .then(() => this.createSocketToApp(retryAttempt));
226 }
227
228 public stop() {
229 if (this.socketToApp) {
230 this.socketToApp.removeAllListeners();
231 this.socketToApp.close();
232 }
233
234 if (this.singleLifetimeWorker) {
235 this.singleLifetimeWorker.stop();
236 }
237 }
238
239 public downloadAndPatchDebuggerWorker(): Q.Promise<void> {
240 let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
241 return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath, this.projectRootPath, this.debuggerWorkerUrlPath)
242 .then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
243 .then((workerContent: string) => {
244 const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
245 // Add our customizations to debugger worker to get it working smoothly
246 // in Node env and polyfill WebWorkers API over Node's IPC.
247 const modifiedDebuggeeContent = [
248 MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
249 MultipleLifetimesAppWorker.CONSOLE_TRACE_PATCH,
250 isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
251 workerContent,
252 MultipleLifetimesAppWorker.WORKER_DONE,
253 ].join("\n");
254 return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
255 });
256 }
257
258 private startNewWorkerLifetime(): Q.Promise<void> {
259 this.singleLifetimeWorker = new ForkedAppWorker(this.packagerAddress, this.packagerPort, this.sourcesStoragePath, this.projectRootPath,
260 (message) => {
261 this.sendMessageToApp(message);
262 },
263 this.packagerRemoteRoot, this.packagerLocalRoot);
264 logger.verbose("A new app worker lifetime was created.");
265 return this.singleLifetimeWorker.start()
266 .then(startedEvent => {
267 this.emit("connected", startedEvent);
268 });
269 }
270
271 private createSocketToApp(retryAttempt: boolean = false): Q.Promise<void> {
272 let deferred = Q.defer<void>();
273 this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
274 this.socketToApp.on("open", () => {
275 this.onSocketOpened();
276 });
277 this.socketToApp.on("close",
278 () => {
279 this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
280 /*
281 * It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
282 * it closes the socket because it already has a connection to a debugger.
283 * https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
284 */
285 let msgKey = "_closeMessage";
286 if (this.socketToApp[msgKey] === "Another debugger is already connected") {
287 deferred.reject(ErrorHelper.getInternalError(InternalErrorCode.AnotherDebuggerConnectedToPackager));
288 }
289 logger.log(localize("DisconnectedFromThePackagerToReactNative", "Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon..."));
290 });
291 setTimeout(() => {
292 this.start(true /* retryAttempt */);
293 }, 100);
294 });
295 this.socketToApp.on("message",
296 (message: any) => this.onMessage(message));
297 this.socketToApp.on("error",
298 (error: Error) => {
299 if (retryAttempt) {
300 printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.ReconnectionToPackagerFailedCheckForErrorsOrRestartReactNative), error);
301 }
302
303 deferred.reject(error);
304 });
305
306 // In an attempt to catch failures in starting the packager on first attempt,
307 // wait for 300 ms before resolving the promise
308 Q.delay(300).done(() => deferred.resolve(void 0));
309 return deferred.promise;
310 }
311
312 private debuggerProxyUrl() {
313 return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
314 }
315
316 private onSocketOpened() {
317 this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
318 logger.log(localize("EstablishedConnectionWithPackagerToReactNativeApp", "Established a connection with the Proxy (Packager) to the React Native application")));
319 }
320
321 private killWorker() {
322 if (!this.singleLifetimeWorker) return;
323 this.singleLifetimeWorker.stop();
324 this.singleLifetimeWorker = null;
325 }
326
327 private onMessage(message: string) {
328 try {
329 logger.verbose("From RN APP: " + message);
330 let object = <RNAppMessage>JSON.parse(message);
331 if (object.method === "prepareJSRuntime") {
332 // In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
333 // when user reloads an app, hence we need to try to kill it here either.
334 this.killWorker();
335 // The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
336 this.gotPrepareJSRuntime(object);
337 } else if (object.method === "$disconnected") {
338 // We need to shutdown the current app worker, and create a new lifetime
339 this.killWorker();
340 } else if (object.method) {
341 // All the other messages are handled by the single lifetime worker
342 if (this.singleLifetimeWorker) {
343 this.singleLifetimeWorker.postMessage(object);
344 }
345 } else {
346 // Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
347 logger.verbose(`The react-native app sent a message without specifying a method: ${message}`);
348 }
349 } catch (exception) {
350 printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToProcessMessageFromReactNativeApp, message), exception);
351 }
352 }
353
354 private gotPrepareJSRuntime(message: any): void {
355 // Create the sandbox, and replay that we finished processing the message
356 this.startNewWorkerLifetime().done(() => {
357 this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
358 }, error => printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToPrepareJSRuntimeEnvironment, message), error));
359 }
360
361 private sendMessageToApp(message: any): void {
362 let stringified: string = "";
363 try {
364 stringified = JSON.stringify(message);
365 logger.verbose(`To RN APP: ${stringified}`);
366 this.socketToApp.send(stringified);
367 } catch (exception) {
368 let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
369 printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToSendMessageToTheReactNativeApp, messageToShow), exception);
370 }
371 }
372}
373