microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
bb77358c8dc7ea46fae9d6aa601a11fde8eed0fd

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

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