microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
dd90a8563dcc503fd324eb3f78da942af96c4109

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

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