microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
923c8dea9d48d22c0579d6cab90e8782458368dc

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

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