microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
a13247045de9faef3c9fcb9dcfc530a08b94838f

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

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