microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
ea8a5f88eea22e059c668cbca7ca4bc4bfa20dbe

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

222lines · modeblame

9f036952Nisheet Jain10 years ago1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4677921cdigeff10 years ago4import * as vm from "vm";
5import * as Q from "q";
6import * as path from "path";
ea8a5f88digeff10 years ago7import * as WebSocket from "ws";
4677921cdigeff10 years ago8import {ScriptImporter} from "./scriptImporter";
9import {Packager} from "./packager";
ea8a5f88digeff10 years ago10import {Log, LogLevel} from "../utils/commands/log";
5d4d4de0digeff10 years ago11import {Node} from "../utils/node/node";
4677921cdigeff10 years ago12
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;
20self: DebuggerWorkerSandbox;
ea8a5f88digeff10 years ago21console: typeof console;
22require: (id: string) => any;
4677921cdigeff10 years ago23importScripts: (url: string) => void;
24postMessage: (object: any) => void;
ea8a5f88digeff10 years ago25onmessage: (object: RNAppMessage) => void;
26postMessageArgument: RNAppMessage; // We use this argument to pass messages to the worker
4677921cdigeff10 years ago27}
28
ea8a5f88digeff10 years ago29interface RNAppMessage {
30method: string;
31// These objects have also other properties but that we don't currently use
32}
5d4d4de0digeff10 years ago33
34function printDebuggingFatalError(message: string, reason: any) {
fb8f49fbMeena Kunnathur Balakrishnan10 years ago35Log.logError(`${message}. Debugging won't work: Try reloading the JS from inside the app, or Reconnect the VS Code debugger`, reason);
5d4d4de0digeff10 years ago36}
37
4677921cdigeff10 years ago38export class SandboxedAppWorker {
ea8a5f88digeff10 years ago39/** 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*/
4677921cdigeff10 years ago46private sourcesStoragePath: string;
47private postReplyToApp: (message: any) => void;
48
49private sandbox: DebuggerWorkerSandbox;
50private sandboxContext: vm.Context;
ea8a5f88digeff10 years ago51private scriptToReceiveMessageInSandbox: vm.Script;
4677921cdigeff10 years ago52
5d4d4de0digeff10 years ago53private pendingScriptImport = Q(void 0);
4677921cdigeff10 years ago54
ea8a5f88digeff10 years ago55private static PROCESS_MESSAGE_INSIDE_SANDBOX = "onmessage({ data: postMessageArgument });";
56
4677921cdigeff10 years ago57constructor(sourcesStoragePath: string, postReplyToApp: (message: any) => void) {
58this.sourcesStoragePath = sourcesStoragePath;
59this.postReplyToApp = postReplyToApp;
ea8a5f88digeff10 years ago60this.scriptToReceiveMessageInSandbox = new vm.Script(SandboxedAppWorker.PROCESS_MESSAGE_INSIDE_SANDBOX);
4677921cdigeff10 years ago61}
62
5d4d4de0digeff10 years ago63public start(): Q.Promise<void> {
64let scriptToRunPath = require.resolve(path.join(this.sourcesStoragePath, Packager.DEBUGGER_WORKER_FILE_BASENAME));
4677921cdigeff10 years ago65this.initializeSandboxAndContext(scriptToRunPath);
5d4d4de0digeff10 years ago66return 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)
70this.runInSandbox(scriptToRunPath, "var onmessage = null; " + fileContents));
4677921cdigeff10 years ago71}
72
ea8a5f88digeff10 years ago73public postMessage(object: RNAppMessage): void {
74this.sandbox.postMessageArgument = object;
75this.scriptToReceiveMessageInSandbox.runInContext(this.sandboxContext);
4677921cdigeff10 years ago76}
77
78private initializeSandboxAndContext(scriptToRunPath: string): void {
79let scriptToRunModule = new Module(scriptToRunPath);
80
81this.sandbox = {
82__filename: scriptToRunPath,
83__dirname: path.dirname(scriptToRunPath),
84self: null,
85console: console,
86require: (filePath: string) => scriptToRunModule.require(filePath), // Give the sandbox access to require("<filePath>");
87importScripts: (url: string) => this.importScripts(url), // Import script like using <script/>
88postMessage: (object: any) => this.gotResponseFromDebuggerWorker(object), // Post message back to the UI thread
ea8a5f88digeff10 years ago89onmessage: null,
90postMessageArgument: null
4677921cdigeff10 years ago91};
92this.sandbox.self = this.sandbox;
93
94this.sandboxContext = vm.createContext(this.sandbox);
95}
96
b3a793eeNisheet Jain10 years ago97private runInSandbox(filename: string, fileContents?: string): Q.Promise<void> {
98let fileContentsPromise = fileContents
99? Q(fileContents)
100: this.readFileContents(filename);
101
102return fileContentsPromise.then(contents => {
103vm.runInContext(contents, this.sandboxContext, filename);
104});
105}
106
107private readFileContents(filename: string) {
108return new Node.FileSystem().readFile(filename).then(contents => contents.toString());
109}
110
4677921cdigeff10 years ago111private importScripts(url: string): void {
5d4d4de0digeff10 years ago112/* The debuggerWorker.js executes this code:
113importScripts(message.url);
114sendReply();
115
116In 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
117actually send the reply back to the application until after importScripts has finished executing. We use
118this.pendingScriptImport to make the gotResponseFromDebuggerWorker() method hold the reply back, until've finished importing
119and running the script */
4677921cdigeff10 years ago120let defer = Q.defer<{}>();
5d4d4de0digeff10 years ago121this.pendingScriptImport = defer.promise;
4677921cdigeff10 years ago122
123// The next line converts to any due to the incorrect typing on node.d.ts of vm.runInThisContext
124new ScriptImporter(this.sourcesStoragePath).download(url)
125.then(downloadedScript =>
126this.runInSandbox(downloadedScript.filepath, downloadedScript.contents))
5d4d4de0digeff10 years ago127.done(() => {
9d7db611digeff10 years ago128// Now we let the reply to the app proceed
129defer.resolve({});
130}, reason => {
131printDebuggingFatalError(`Couldn't import script at <${url}>`, reason);
132});
4677921cdigeff10 years ago133}
134
135private gotResponseFromDebuggerWorker(object: any): void {
5d4d4de0digeff10 years ago136// We might need to hold the response until a script is imported. See comments on this.importScripts()
137this.pendingScriptImport.done(() =>
4677921cdigeff10 years ago138this.postReplyToApp(object));
139}
140}
141
142export class MultipleLifetimesAppWorker {
ea8a5f88digeff10 years ago143/** 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*/
4677921cdigeff10 years ago148private sourcesStoragePath: string;
ea8a5f88digeff10 years ago149private socketToApp: WebSocket;
4677921cdigeff10 years ago150private singleLifetimeWorker: SandboxedAppWorker;
151
152constructor(sourcesStoragePath: string) {
153this.sourcesStoragePath = sourcesStoragePath;
ea8a5f88digeff10 years ago154console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
4677921cdigeff10 years ago155}
156
5d4d4de0digeff10 years ago157public start(): Q.Promise<void> {
158this.singleLifetimeWorker = new SandboxedAppWorker(this.sourcesStoragePath, (message) => {
159this.sendMessageToApp(message);
160});
161return this.singleLifetimeWorker.start().then(() => {
162this.socketToApp = this.createSocketToApp();
163});
4677921cdigeff10 years ago164}
165
166private createSocketToApp() {
167let socketToApp = new WebSocket(this.debuggerProxyUrl());
ea8a5f88digeff10 years ago168socketToApp.on("open", () =>
169this.onSocketOpened());
170socketToApp.on("close", () =>
171this.onSocketClose());
172socketToApp.on("message",
173(message: any) => this.onMessage(message));
174socketToApp.on("error",
175(error: Error) => printDebuggingFatalError("An error ocurred while using the socket to communicate with the React Native app", error));
4677921cdigeff10 years ago176return socketToApp;
177}
178
179private debuggerProxyUrl() {
180return `ws://${Packager.HOST}/debugger-proxy`;
181}
182
ea8a5f88digeff10 years ago183private onSocketOpened() {
4677921cdigeff10 years ago184Log.logMessage("Established a connection with the Proxy (Packager) to the React Native application");
185}
186
ea8a5f88digeff10 years ago187private onSocketClose() {
5d4d4de0digeff10 years ago188// TODO: Add some logic to not print this message that often, we'll spam the user
4677921cdigeff10 years ago189Log.logMessage("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...");
5d4d4de0digeff10 years ago190setTimeout(() => this.start(), 100);
4677921cdigeff10 years ago191}
192
ea8a5f88digeff10 years ago193private onMessage(message: string) {
5d4d4de0digeff10 years ago194try {
ea8a5f88digeff10 years ago195Log.logInternalMessage(LogLevel.Trace, "From RN APP: " + message);
196let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago197if (object.method === "prepareJSRuntime") {
198// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
199this.gotPrepareJSRuntime(object);
200} else if (object.method) {
201// All the other messages are handled by the single lifetime worker
202this.singleLifetimeWorker.postMessage(object);
203} else {
ea8a5f88digeff10 years ago204// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
205Log.logInternalMessage(LogLevel.Info, "The react-native app sent a message without specifying a method: " + message);
5d4d4de0digeff10 years ago206}
207} catch (exception) {
208printDebuggingFatalError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
4677921cdigeff10 years ago209}
210}
211
212private gotPrepareJSRuntime(message: any): void {
213// Create the sandbox, and replay that we finished processing the message
214this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
215}
216
217private sendMessageToApp(message: any) {
ea8a5f88digeff10 years ago218let stringified = JSON.stringify(message);
219Log.logInternalMessage(LogLevel.Trace, "To RN APP: " + stringified);
220this.socketToApp.send(stringified);
4677921cdigeff10 years ago221}
222}