microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
efd8fbff90ccf6d8b6743b251723b1ee27e9a2bb

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

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