microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e4c8f7d41089bcd9e9b4f110965683420bc95886

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

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