microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0555a744dc42f0c954fb9335bee33d880103771c

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

191lines · 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, true);
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 this.sandbox.onmessage({ data: object });
72 }
73
74 private initializeSandboxAndContext(scriptToRunPath: string): void {
75 let scriptToRunModule = new Module(scriptToRunPath);
76
77 this.sandbox = {
78 __filename: scriptToRunPath,
79 __dirname: path.dirname(scriptToRunPath),
80 self: null,
81 console: console,
82 require: (filePath: string) => scriptToRunModule.require(filePath), // Give the sandbox access to require("<filePath>");
83 importScripts: (url: string) => this.importScripts(url), // Import script like using <script/>
84 postMessage: (object: any) => this.gotResponseFromDebuggerWorker(object), // Post message back to the UI thread
85 onmessage: null
86 };
87 this.sandbox.self = this.sandbox;
88
89 this.sandboxContext = vm.createContext(this.sandbox);
90 }
91
92 private importScripts(url: string): void {
93 /* The debuggerWorker.js executes this code:
94 importScripts(message.url);
95 sendReply();
96
97 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
98 actually send the reply back to the application until after importScripts has finished executing. We use
99 this.pendingScriptImport to make the gotResponseFromDebuggerWorker() method hold the reply back, until've finished importing
100 and running the script */
101 let defer = Q.defer<{}>();
102 this.pendingScriptImport = defer.promise;
103
104 // The next line converts to any due to the incorrect typing on node.d.ts of vm.runInThisContext
105 new ScriptImporter(this.sourcesStoragePath).download(url)
106 .then(downloadedScript =>
107 this.runInSandbox(downloadedScript.filepath, downloadedScript.contents))
108 .done(() => {
109 // Now we let the reply to the app proceed
110 defer.resolve({});
111 }, reason => {
112 printDebuggingFatalError(`Couldn't import script at <${url}>`, reason);
113 });
114 }
115
116 private gotResponseFromDebuggerWorker(object: any): void {
117 // We might need to hold the response until a script is imported. See comments on this.importScripts()
118 this.pendingScriptImport.done(() =>
119 this.postReplyToApp(object));
120 }
121}
122
123export class MultipleLifetimesAppWorker {
124 private sourcesStoragePath: string;
125 private socketToApp: any;
126 private singleLifetimeWorker: SandboxedAppWorker;
127
128 constructor(sourcesStoragePath: string) {
129 this.sourcesStoragePath = sourcesStoragePath;
130 }
131
132 public start(): Q.Promise<void> {
133 this.singleLifetimeWorker = new SandboxedAppWorker(this.sourcesStoragePath, (message) => {
134 this.sendMessageToApp(message);
135 });
136 return this.singleLifetimeWorker.start().then(() => {
137 this.socketToApp = this.createSocketToApp();
138 });
139 }
140
141 private createSocketToApp() {
142 let socketToApp = new WebSocket(this.debuggerProxyUrl());
143 socketToApp.onopen = () => this.socketToAppWasOpened();
144 socketToApp.onclose = () => this.socketWasClosed();
145 socketToApp.onmessage = (message: any) => this.messageReceivedFromApp(message);
146 // TODO: Add on error handler
147 return socketToApp;
148 }
149
150 private debuggerProxyUrl() {
151 return `ws://${Packager.HOST}/debugger-proxy`;
152 }
153
154 private socketToAppWasOpened() {
155 Log.logMessage("Established a connection with the Proxy (Packager) to the React Native application");
156 }
157
158 private socketWasClosed() {
159 // TODO: Add some logic to not print this message that often, we'll spam the user
160 Log.logMessage("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...");
161 setTimeout(() => this.start(), 100);
162 }
163
164 // TODO: Add proper typings for message
165 private messageReceivedFromApp(message: any) {
166 try {
167 let object = JSON.parse(message.data);
168 if (object.method === "prepareJSRuntime") {
169 // The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
170 this.gotPrepareJSRuntime(object);
171 } else if (object.method) {
172 // All the other messages are handled by the single lifetime worker
173 this.singleLifetimeWorker.postMessage(object);
174 } else {
175 // Message doesn't have a method. Ignore it.
176 Log.logInternalMessage("The react-native app sent a message without specifying a method: " + message);
177 }
178 } catch (exception) {
179 printDebuggingFatalError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
180 }
181 }
182
183 private gotPrepareJSRuntime(message: any): void {
184 // Create the sandbox, and replay that we finished processing the message
185 this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
186 }
187
188 private sendMessageToApp(message: any) {
189 this.socketToApp.send(JSON.stringify(message));
190 }
191}
192