microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
b0061ac657ace1a09c59d28397cb8a1b6009bc2d

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

222lines · 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";
12
13import Module = require("module");
14
15// This file is a replacement of: https://github.com/facebook/react-native/blob/8d397b4cbc05ad801cfafb421cee39bcfe89711d/local-cli/server/util/debugger.html for Node.JS
16
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 printDebuggingFatalError(message: string, reason: any) {
35 Log.logError(`${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 postReplyToApp: (message: any) => void;
48
49 private sandbox: DebuggerWorkerSandbox;
50 private sandboxContext: vm.Context;
51 private scriptToReceiveMessageInSandbox: vm.Script;
52
53 private pendingScriptImport = Q(void 0);
54
55 private static PROCESS_MESSAGE_INSIDE_SANDBOX = "onmessage({ data: postMessageArgument });";
56
57 constructor(sourcesStoragePath: string, postReplyToApp: (message: any) => void) {
58 this.sourcesStoragePath = sourcesStoragePath;
59 this.postReplyToApp = postReplyToApp;
60 this.scriptToReceiveMessageInSandbox = new vm.Script(SandboxedAppWorker.PROCESS_MESSAGE_INSIDE_SANDBOX);
61 }
62
63 public start(): Q.Promise<void> {
64 let scriptToRunPath = require.resolve(path.join(this.sourcesStoragePath, Packager.DEBUGGER_WORKER_FILE_BASENAME));
65 this.initializeSandboxAndContext(scriptToRunPath);
66 return this.readFileContents(scriptToRunPath).then(fileContents =>
67 // On a debugger worker the onmessage variable already exist. We need to declare it before the
68 // javascript file can assign it. We do it in the first line without a new line to not break
69 // the debugging experience of debugging debuggerWorker.js itself (as part of the extension)
70 this.runInSandbox(scriptToRunPath, "var onmessage = null; " + fileContents));
71 }
72
73 public postMessage(object: RNAppMessage): void {
74 this.sandbox.postMessageArgument = object;
75 this.scriptToReceiveMessageInSandbox.runInContext(this.sandboxContext);
76 }
77
78 private initializeSandboxAndContext(scriptToRunPath: string): void {
79 let scriptToRunModule = new Module(scriptToRunPath);
80
81 this.sandbox = {
82 __filename: scriptToRunPath,
83 __dirname: path.dirname(scriptToRunPath),
84 self: null,
85 console: console,
86 require: (filePath: string) => scriptToRunModule.require(filePath), // Give the sandbox access to require("<filePath>");
87 importScripts: (url: string) => this.importScripts(url), // Import script like using <script/>
88 postMessage: (object: any) => this.gotResponseFromDebuggerWorker(object), // Post message back to the UI thread
89 onmessage: null,
90 postMessageArgument: null
91 };
92 this.sandbox.self = this.sandbox;
93
94 this.sandboxContext = vm.createContext(this.sandbox);
95 }
96
97 private runInSandbox(filename: string, fileContents?: string): Q.Promise<void> {
98 let fileContentsPromise = fileContents
99 ? Q(fileContents)
100 : this.readFileContents(filename);
101
102 return fileContentsPromise.then(contents => {
103 vm.runInContext(contents, this.sandboxContext, filename);
104 });
105 }
106
107 private readFileContents(filename: string) {
108 return new Node.FileSystem().readFile(filename).then(contents => contents.toString());
109 }
110
111 private importScripts(url: string): void {
112 /* The debuggerWorker.js executes this code:
113 importScripts(message.url);
114 sendReply();
115
116 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
117 actually send the reply back to the application until after importScripts has finished executing. We use
118 this.pendingScriptImport to make the gotResponseFromDebuggerWorker() method hold the reply back, until've finished importing
119 and running the script */
120 let defer = Q.defer<{}>();
121 this.pendingScriptImport = defer.promise;
122
123 // The next line converts to any due to the incorrect typing on node.d.ts of vm.runInThisContext
124 new ScriptImporter(this.sourcesStoragePath).download(url)
125 .then(downloadedScript =>
126 this.runInSandbox(downloadedScript.filepath, downloadedScript.contents))
127 .done(() => {
128 // Now we let the reply to the app proceed
129 defer.resolve({});
130 }, reason => {
131 printDebuggingFatalError(`Couldn't import script at <${url}>`, reason);
132 });
133 }
134
135 private gotResponseFromDebuggerWorker(object: any): void {
136 // We might need to hold the response until a script is imported. See comments on this.importScripts()
137 this.pendingScriptImport.done(() =>
138 this.postReplyToApp(object));
139 }
140}
141
142export class MultipleLifetimesAppWorker {
143 /** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
144 * and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
145 * is the prepareJSRuntime, which we reply to the RN App that the sandbox was created succesfully.
146 * When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
147 */
148 private sourcesStoragePath: string;
149 private socketToApp: WebSocket;
150 private singleLifetimeWorker: SandboxedAppWorker;
151
152 constructor(sourcesStoragePath: string) {
153 this.sourcesStoragePath = sourcesStoragePath;
154 console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
155 }
156
157 public start(): Q.Promise<void> {
158 this.singleLifetimeWorker = new SandboxedAppWorker(this.sourcesStoragePath, (message) => {
159 this.sendMessageToApp(message);
160 });
161 return this.singleLifetimeWorker.start().then(() => {
162 this.socketToApp = this.createSocketToApp();
163 });
164 }
165
166 private createSocketToApp() {
167 let socketToApp = new WebSocket(this.debuggerProxyUrl());
168 socketToApp.on("open", () =>
169 this.onSocketOpened());
170 socketToApp.on("close", () =>
171 this.onSocketClose());
172 socketToApp.on("message",
173 (message: any) => this.onMessage(message));
174 socketToApp.on("error",
175 (error: Error) => printDebuggingFatalError("An error ocurred while using the socket to communicate with the React Native app", error));
176 return socketToApp;
177 }
178
179 private debuggerProxyUrl() {
180 return `ws://${Packager.HOST}/debugger-proxy`;
181 }
182
183 private onSocketOpened() {
184 Log.logMessage("Established a connection with the Proxy (Packager) to the React Native application");
185 }
186
187 private onSocketClose() {
188 // TODO: Add some logic to not print this message that often, we'll spam the user
189 Log.logMessage("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...");
190 setTimeout(() => this.start(), 100);
191 }
192
193 private onMessage(message: string) {
194 try {
195 Log.logInternalMessage(LogLevel.Trace, "From RN APP: " + message);
196 let object = <RNAppMessage>JSON.parse(message);
197 if (object.method === "prepareJSRuntime") {
198 // The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
199 this.gotPrepareJSRuntime(object);
200 } else if (object.method) {
201 // All the other messages are handled by the single lifetime worker
202 this.singleLifetimeWorker.postMessage(object);
203 } else {
204 // Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
205 Log.logInternalMessage(LogLevel.Info, "The react-native app sent a message without specifying a method: " + message);
206 }
207 } catch (exception) {
208 printDebuggingFatalError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
209 }
210 }
211
212 private gotPrepareJSRuntime(message: any): void {
213 // Create the sandbox, and replay that we finished processing the message
214 this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
215 }
216
217 private sendMessageToApp(message: any) {
218 let stringified = JSON.stringify(message);
219 Log.logInternalMessage(LogLevel.Trace, "To RN APP: " + stringified);
220 this.socketToApp.send(stringified);
221 }
222}
223