microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
df4bce4041caa61af1460ef87f2380820508a455

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

279lines · 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 vm from "vm";
5import * as Q from "q";
6import * as path from "path";
7import * as WebSocket from "ws";
8import {ScriptImporter} from "./scriptImporter";
9import {Packager} from "../common/packager";
10import {ErrorHelper} from "../common/error/errorHelper";
11import {Log} from "../common/log/log";
12import {LogLevel} from "../common/log/logHelper";
13import {FileSystem} from "../common/node/fileSystem";
14import {ExecutionsLimiter} from "../common/executionsLimiter";
15
16import Module = require("module");
17
18// This file is a replacement of: https://github.com/facebook/react-native/blob/8d397b4cbc05ad801cfafb421cee39bcfe89711d/local-cli/server/util/debugger.html for Node.JS
19interface DebuggerWorkerSandbox {
20 __filename: string;
21 __dirname: string;
22 self: DebuggerWorkerSandbox;
23 console: typeof console;
24 require: (id: string) => any;
25 importScripts: (url: string) => void;
26 postMessage: (object: any) => void;
27 onmessage: (object: RNAppMessage) => void;
28 postMessageArgument: RNAppMessage; // We use this argument to pass messages to the worker
29}
30
31interface RNAppMessage {
32 method: string;
33 // These objects have also other properties but that we don't currently use
34}
35
36function printDebuggingError(message: string, reason: any) {
37 Log.logWarning(ErrorHelper.getNestedWarning(reason, `${message}. Debugging won't work: Try reloading the JS from inside the app, or Reconnect the VS Code debugger`));
38}
39
40export class SandboxedAppWorker {
41 /** This class will run the RN App logic inside a sandbox. The framework to run the logic is provided by the file
42 * debuggerWorker.js (designed to run on a WebWorker). We load that file inside a sandbox, and then we use the
43 * PROCESS_MESSAGE_INSIDE_SANDBOX script to execute the logic to respond to a message inside the sandbox.
44 * The code inside the debuggerWorker.js will call the global function postMessage to send a reply back to the app,
45 * so we define our custom function there, so we can handle the message. We also provide our own importScript function
46 * to download any script used by debuggerWorker.js
47 */
48 private sourcesStoragePath: string;
49 private debugAdapterPort: number;
50 private postReplyToApp: (message: any) => void;
51
52 private sandbox: DebuggerWorkerSandbox;
53 private sandboxContext: vm.Context;
54 private scriptToReceiveMessageInSandbox: vm.Script;
55
56 private pendingScriptImport = Q(void 0);
57
58 private nodeFileSystem: FileSystem;
59 private scriptImporter: ScriptImporter;
60
61 private static PROCESS_MESSAGE_INSIDE_SANDBOX = "onmessage({ data: postMessageArgument });";
62
63 constructor(private packagerPort: number, sourcesStoragePath: string, debugAdapterPort: number, postReplyToApp: (message: any) => void, {
64 nodeFileSystem = new FileSystem(),
65 scriptImporter = new ScriptImporter(packagerPort, sourcesStoragePath),
66 } = {}) {
67 this.sourcesStoragePath = sourcesStoragePath;
68 this.debugAdapterPort = debugAdapterPort;
69 this.postReplyToApp = postReplyToApp;
70 this.scriptToReceiveMessageInSandbox = new vm.Script(SandboxedAppWorker.PROCESS_MESSAGE_INSIDE_SANDBOX);
71
72 this.nodeFileSystem = nodeFileSystem;
73 this.scriptImporter = scriptImporter;
74 }
75
76 public start(): Q.Promise<void> {
77 let scriptToRunPath = require.resolve(path.join(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILE_BASENAME));
78 this.initializeSandboxAndContext(scriptToRunPath);
79 return this.readFileContents(scriptToRunPath).then(fileContents =>
80 // On a debugger worker the onmessage variable already exist. We need to declare it before the
81 // javascript file can assign it. We do it in the first line without a new line to not break
82 // the debugging experience of debugging debuggerWorker.js itself (as part of the extension)
83 this.runInSandbox(scriptToRunPath, "var onmessage = null; " + fileContents));
84 }
85
86 public postMessage(object: RNAppMessage): void {
87 this.sandbox.postMessageArgument = object;
88 this.scriptToReceiveMessageInSandbox.runInContext(this.sandboxContext);
89 }
90
91 private initializeSandboxAndContext(scriptToRunPath: string): void {
92 let scriptToRunModule = new Module(scriptToRunPath);
93
94 this.sandbox = {
95 __filename: scriptToRunPath,
96 __dirname: path.dirname(scriptToRunPath),
97 self: null,
98 console: console,
99 require: (filePath: string) => scriptToRunModule.require(filePath), // Give the sandbox access to require("<filePath>");
100 importScripts: (url: string) => this.importScripts(url), // Import script like using <script/>
101 postMessage: (object: any) => this.gotResponseFromDebuggerWorker(object), // Post message back to the UI thread
102 onmessage: null,
103 postMessageArgument: null,
104 };
105 this.sandbox.self = this.sandbox;
106
107 this.sandboxContext = vm.createContext(this.sandbox);
108 }
109
110 private runInSandbox(filename: string, fileContents?: string): Q.Promise<void> {
111 let fileContentsPromise = fileContents
112 ? Q(fileContents)
113 : this.readFileContents(filename);
114
115 return fileContentsPromise.then(contents => {
116 vm.runInContext(contents, this.sandboxContext, filename);
117 });
118 }
119
120 private readFileContents(filename: string) {
121 return this.nodeFileSystem.readFile(filename).then(contents => contents.toString());
122 }
123
124 private importScripts(url: string): void {
125 /* The debuggerWorker.js executes this code:
126 importScripts(message.url);
127 sendReply();
128
129 In the original code importScripts is a sync call. In our code it's async, so we need to mess with sendReply() so we won't
130 actually send the reply back to the application until after importScripts has finished executing. We use
131 this.pendingScriptImport to make the gotResponseFromDebuggerWorker() method hold the reply back, until've finished importing
132 and running the script */
133 let defer = Q.defer<{}>();
134 this.pendingScriptImport = defer.promise;
135
136 // The next line converts to any due to the incorrect typing on node.d.ts of vm.runInThisContext
137 this.scriptImporter.downloadAppScript(url, this.debugAdapterPort)
138 .then(downloadedScript =>
139 this.runInSandbox(downloadedScript.filepath, downloadedScript.contents))
140 .done(() => {
141 // Now we let the reply to the app proceed
142 defer.resolve({});
143 }, reason => {
144 printDebuggingError(`Couldn't import script at <${url}>`, reason);
145 });
146 }
147
148 private gotResponseFromDebuggerWorker(object: any): void {
149 // We might need to hold the response until a script is imported. See comments on this.importScripts()
150 this.pendingScriptImport.done(() =>
151 this.postReplyToApp(object), reason => {
152 printDebuggingError("Unexpected internal error while processing a message from the RN App.", reason);
153 });
154 }
155}
156
157export class MultipleLifetimesAppWorker {
158 /** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
159 * and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
160 * is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
161 * When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
162 */
163 private sourcesStoragePath: string;
164 private debugAdapterPort: number;
165 private socketToApp: WebSocket;
166 private singleLifetimeWorker: SandboxedAppWorker;
167
168 private sandboxedAppConstructor: (storagePath: string, adapterPort: number, messageFunction: (message: any) => void) => SandboxedAppWorker;
169 private webSocketConstructor: (url: string) => WebSocket;
170
171 private executionLimiter = new ExecutionsLimiter();
172
173 constructor(private packagerPort: number, sourcesStoragePath: string, debugAdapterPort: number, {
174 sandboxedAppConstructor = (path: string, port: number, messageFunc: (message: any) => void) =>
175 new SandboxedAppWorker(packagerPort, path, port, messageFunc),
176 webSocketConstructor = (url: string) => new WebSocket(url),
177 } = {}) {
178 this.sourcesStoragePath = sourcesStoragePath;
179 this.debugAdapterPort = debugAdapterPort;
180 console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
181
182 this.sandboxedAppConstructor = sandboxedAppConstructor;
183 this.webSocketConstructor = webSocketConstructor;
184 }
185
186 public start(warnOnFailure: boolean = false): Q.Promise<any> {
187 return this.createSocketToApp(warnOnFailure);
188 }
189
190 private startNewWorkerLifetime(): Q.Promise<void> {
191 this.singleLifetimeWorker = this.sandboxedAppConstructor(this.sourcesStoragePath, this.debugAdapterPort, (message) => {
192 this.sendMessageToApp(message);
193 });
194 Log.logInternalMessage(LogLevel.Info, "A new app worker lifetime was created.");
195 return this.singleLifetimeWorker.start();
196 }
197
198 private createSocketToApp(warnOnFailure: boolean = false): Q.Promise<void> {
199 let deferred = Q.defer<void>();
200 this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
201 this.socketToApp.on("open", () => {
202 this.onSocketOpened();
203 });
204 this.socketToApp.on("close", () =>
205 this.onSocketClose());
206 this.socketToApp.on("message",
207 (message: any) => this.onMessage(message));
208 this.socketToApp.on("error",
209 (error: Error) => {
210 if (warnOnFailure) {
211 Log.logWarning(ErrorHelper.getNestedWarning(error,
212 "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."));
213 }
214
215 deferred.reject(error);
216 });
217
218 // In an attempt to catch failures in starting the packager on first attempt,
219 // wait for 300 ms before resolving the promise
220 Q.delay(300).done(() => deferred.resolve(void 0));
221 return deferred.promise;
222 }
223
224 private debuggerProxyUrl() {
225 return `ws://${Packager.getHostForPort(this.packagerPort)}/debugger-proxy?role=debugger&name=vscode`;
226 }
227
228 private onSocketOpened() {
229 this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
230 Log.logMessage("Established a connection with the Proxy (Packager) to the React Native application"));
231 }
232
233 private onSocketClose() {
234 this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () =>
235 Log.logMessage("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon..."));
236 setTimeout(() => this.start(true /* retryAttempt */), 100);
237 }
238
239 private onMessage(message: string) {
240 try {
241 Log.logInternalMessage(LogLevel.Trace, "From RN APP: " + message);
242 let object = <RNAppMessage>JSON.parse(message);
243 if (object.method === "prepareJSRuntime") {
244 // The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
245 this.gotPrepareJSRuntime(object);
246 } else if (object.method === "$disconnected") {
247 // We need to shutdown the current app worker, and create a new lifetime
248 this.singleLifetimeWorker = null;
249 } else if (object.method) {
250 // All the other messages are handled by the single lifetime worker
251 this.singleLifetimeWorker.postMessage(object);
252 } else {
253 // Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
254 Log.logInternalMessage(LogLevel.Info, "The react-native app sent a message without specifying a method: " + message);
255 }
256 } catch (exception) {
257 printDebuggingError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
258 }
259 }
260
261 private gotPrepareJSRuntime(message: any): void {
262 // Create the sandbox, and replay that we finished processing the message
263 this.startNewWorkerLifetime().done(() => {
264 this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
265 }, error => printDebuggingError(`Failed to prepare the JavaScript runtime environment. Message:\n${message}`, error));
266 }
267
268 private sendMessageToApp(message: any): void {
269 let stringified: string = null;
270 try {
271 stringified = JSON.stringify(message);
272 Log.logInternalMessage(LogLevel.Trace, "To RN APP: " + stringified);
273 this.socketToApp.send(stringified);
274 } catch (exception) {
275 let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
276 printDebuggingError(`Failed to send message to the React Native app. Message:\n${messageToShow}`, exception);
277 }
278 }
279}
280