microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
748105d9c1d909e917309cbb58ed2d80fd77c015

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

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