microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
678db279088f7b3fd6c7888d37be778e758ff688

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

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