microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
4abd784953e5fc372c2a69fb34e32c8363aa25a9

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

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