microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
f8d3243916133136da9c1d8eedef7428db609d7d

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

192lines · modecode

1import * as vm from "vm";
2import * as Q from "q";
3import * as path from "path";
4import * as websocket from "websocket";
5import {ScriptImporter} from "./scriptImporter";
6import {Packager} from "./packager";
7import {Log} from "../utils/commands/log";
8import {Node} from "../utils/node/node";
9
10import Module = require("module");
11
12let WebSocket = (<any>websocket).w3cwebsocket;
13
14// This file is a replacement of: https://github.com/facebook/react-native/blob/8d397b4cbc05ad801cfafb421cee39bcfe89711d/local-cli/server/util/debugger.html for Node.JS
15
16interface DebuggerWorkerSandbox {
17 __filename: string;
18 __dirname: string;
19 self: DebuggerWorkerSandbox;
20 console: any;
21 require: (filePath: string) => any;
22 importScripts: (url: string) => void;
23 postMessage: (object: any) => void;
24 onmessage: (object: any) => void;
25}
26
27
28function printDebuggingFatalError(message: string, reason: any) {
29 Log.logError(`${message}. Debugging won't work: Try reloading the JS from inside the app, or Reconnect the VS Code debugger`, reason);
30}
31
32export class SandboxedAppWorker {
33 private sourcesStoragePath: string;
34 private postReplyToApp: (message: any) => void;
35
36 private sandbox: DebuggerWorkerSandbox;
37 private sandboxContext: vm.Context;
38
39 private pendingScriptImport = Q(void 0);
40
41 constructor(sourcesStoragePath: string, postReplyToApp: (message: any) => void) {
42 this.sourcesStoragePath = sourcesStoragePath;
43 this.postReplyToApp = postReplyToApp;
44 }
45
46 private runInSandbox(filename: string, fileContents?: string): Q.Promise<void> {
47 let fileContentsPromise = fileContents
48 ? Q(fileContents)
49 : this.readFileContents(filename);
50
51 return fileContentsPromise.then(contents => {
52 vm.runInContext(contents, this.sandboxContext, filename);
53 });
54 }
55
56 private readFileContents(filename: string) {
57 return new Node.FileSystem().readFile(filename).then(contents => contents.toString());
58 }
59
60 public start(): Q.Promise<void> {
61 let scriptToRunPath = require.resolve(path.join(this.sourcesStoragePath, Packager.DEBUGGER_WORKER_FILE_BASENAME));
62 this.initializeSandboxAndContext(scriptToRunPath);
63 return this.readFileContents(scriptToRunPath).then(fileContents =>
64 // On a debugger worker the onmessage variable already exist. We need to declare it before the
65 // javascript file can assign it. We do it in the first line without a new line to not break
66 // the debugging experience of debugging debuggerWorker.js itself (as part of the extension)
67 this.runInSandbox(scriptToRunPath, "var onmessage = null; " + fileContents));
68 }
69
70 public postMessage(object: any): void {
71 // TODO: Run this call inside of the sandbox
72 this.sandbox.onmessage({ data: object });
73 }
74
75 private initializeSandboxAndContext(scriptToRunPath: string): void {
76 let scriptToRunModule = new Module(scriptToRunPath);
77
78 this.sandbox = {
79 __filename: scriptToRunPath,
80 __dirname: path.dirname(scriptToRunPath),
81 self: null,
82 console: console,
83 require: (filePath: string) => scriptToRunModule.require(filePath), // Give the sandbox access to require("<filePath>");
84 importScripts: (url: string) => this.importScripts(url), // Import script like using <script/>
85 postMessage: (object: any) => this.gotResponseFromDebuggerWorker(object), // Post message back to the UI thread
86 onmessage: null
87 };
88 this.sandbox.self = this.sandbox;
89
90 this.sandboxContext = vm.createContext(this.sandbox);
91 }
92
93 private importScripts(url: string): void {
94 /* The debuggerWorker.js executes this code:
95 importScripts(message.url);
96 sendReply();
97
98 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
99 actually send the reply back to the application until after importScripts has finished executing. We use
100 this.pendingScriptImport to make the gotResponseFromDebuggerWorker() method hold the reply back, until've finished importing
101 and running the script */
102 let defer = Q.defer<{}>();
103 this.pendingScriptImport = defer.promise;
104
105 // The next line converts to any due to the incorrect typing on node.d.ts of vm.runInThisContext
106 new ScriptImporter(this.sourcesStoragePath).download(url)
107 .then(downloadedScript =>
108 this.runInSandbox(downloadedScript.filepath, downloadedScript.contents))
109 .done(() => {
110 // Now we let the reply to the app proceed
111 defer.resolve({});
112 }, reason => {
113 printDebuggingFatalError(`Couldn't import script at <${url}>`, reason);
114 });
115 }
116
117 private gotResponseFromDebuggerWorker(object: any): void {
118 // We might need to hold the response until a script is imported. See comments on this.importScripts()
119 this.pendingScriptImport.done(() =>
120 this.postReplyToApp(object));
121 }
122}
123
124export class MultipleLifetimesAppWorker {
125 private sourcesStoragePath: string;
126 private socketToApp: any;
127 private singleLifetimeWorker: SandboxedAppWorker;
128
129 constructor(sourcesStoragePath: string) {
130 this.sourcesStoragePath = sourcesStoragePath;
131 }
132
133 public start(): Q.Promise<void> {
134 this.singleLifetimeWorker = new SandboxedAppWorker(this.sourcesStoragePath, (message) => {
135 this.sendMessageToApp(message);
136 });
137 return this.singleLifetimeWorker.start().then(() => {
138 this.socketToApp = this.createSocketToApp();
139 });
140 }
141
142 private createSocketToApp() {
143 let socketToApp = new WebSocket(this.debuggerProxyUrl());
144 socketToApp.onopen = () => this.socketToAppWasOpened();
145 socketToApp.onclose = () => this.socketWasClosed();
146 socketToApp.onmessage = (message: any) => this.messageReceivedFromApp(message);
147 // TODO: Add on error handler
148 return socketToApp;
149 }
150
151 private debuggerProxyUrl() {
152 return `ws://${Packager.HOST}/debugger-proxy`;
153 }
154
155 private socketToAppWasOpened() {
156 Log.logMessage("Established a connection with the Proxy (Packager) to the React Native application");
157 }
158
159 private socketWasClosed() {
160 // TODO: Add some logic to not print this message that often, we'll spam the user
161 Log.logMessage("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...");
162 setTimeout(() => this.start(), 100);
163 }
164
165 // TODO: Add proper typings for message
166 private messageReceivedFromApp(message: any) {
167 try {
168 let object = JSON.parse(message.data);
169 if (object.method === "prepareJSRuntime") {
170 // The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
171 this.gotPrepareJSRuntime(object);
172 } else if (object.method) {
173 // All the other messages are handled by the single lifetime worker
174 this.singleLifetimeWorker.postMessage(object);
175 } else {
176 // Message doesn't have a method. Ignore it.
177 Log.logInternalMessage("The react-native app sent a message without specifying a method: " + message);
178 }
179 } catch (exception) {
180 printDebuggingFatalError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
181 }
182 }
183
184 private gotPrepareJSRuntime(message: any): void {
185 // Create the sandbox, and replay that we finished processing the message
186 this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
187 }
188
189 private sendMessageToApp(message: any) {
190 this.socketToApp.send(JSON.stringify(message));
191 }
192}
193