microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
eba0de58017cd198f69bffca9be3d8e0e4df4d6a

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

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