microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
db80cd4e10b83524bc77c15018effab72cbabeb8

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

234lines · 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";
b0061ac6Meena Kunnathur Balakrishnan10 years ago9import {Packager} from "../common/packager";
10import {Log, LogLevel} from "../common/log";
11import {Node} from "../common/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
ce591c62digeff10 years ago34function printDebuggingError(message: string, reason: any) {
10873e11digeff10 years ago35Log.logWarning(`${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;
5547a16fJimmy Thomson10 years ago47private debugAdapterPort: number;
4677921cdigeff10 years ago48private postReplyToApp: (message: any) => void;
49
50private sandbox: DebuggerWorkerSandbox;
51private sandboxContext: vm.Context;
ea8a5f88digeff10 years ago52private scriptToReceiveMessageInSandbox: vm.Script;
4677921cdigeff10 years ago53
5d4d4de0digeff10 years ago54private pendingScriptImport = Q(void 0);
4677921cdigeff10 years ago55
ea8a5f88digeff10 years ago56private static PROCESS_MESSAGE_INSIDE_SANDBOX = "onmessage({ data: postMessageArgument });";
57
5547a16fJimmy Thomson10 years ago58constructor(sourcesStoragePath: string, debugAdapterPort: number, postReplyToApp: (message: any) => void) {
4677921cdigeff10 years ago59this.sourcesStoragePath = sourcesStoragePath;
2570720bJimmy Thomson10 years ago60this.debugAdapterPort = debugAdapterPort;
4677921cdigeff10 years ago61this.postReplyToApp = postReplyToApp;
ea8a5f88digeff10 years ago62this.scriptToReceiveMessageInSandbox = new vm.Script(SandboxedAppWorker.PROCESS_MESSAGE_INSIDE_SANDBOX);
4677921cdigeff10 years ago63}
64
5d4d4de0digeff10 years ago65public start(): Q.Promise<void> {
2743f19cdlebu10 years ago66let scriptToRunPath = require.resolve(path.join(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILE_BASENAME));
4677921cdigeff10 years ago67this.initializeSandboxAndContext(scriptToRunPath);
5d4d4de0digeff10 years ago68return this.readFileContents(scriptToRunPath).then(fileContents =>
69// On a debugger worker the onmessage variable already exist. We need to declare it before the
70// javascript file can assign it. We do it in the first line without a new line to not break
71// the debugging experience of debugging debuggerWorker.js itself (as part of the extension)
72this.runInSandbox(scriptToRunPath, "var onmessage = null; " + fileContents));
4677921cdigeff10 years ago73}
74
ea8a5f88digeff10 years ago75public postMessage(object: RNAppMessage): void {
76this.sandbox.postMessageArgument = object;
77this.scriptToReceiveMessageInSandbox.runInContext(this.sandboxContext);
4677921cdigeff10 years ago78}
79
80private initializeSandboxAndContext(scriptToRunPath: string): void {
81let scriptToRunModule = new Module(scriptToRunPath);
82
83this.sandbox = {
84__filename: scriptToRunPath,
85__dirname: path.dirname(scriptToRunPath),
86self: null,
87console: console,
88require: (filePath: string) => scriptToRunModule.require(filePath), // Give the sandbox access to require("<filePath>");
89importScripts: (url: string) => this.importScripts(url), // Import script like using <script/>
90postMessage: (object: any) => this.gotResponseFromDebuggerWorker(object), // Post message back to the UI thread
ea8a5f88digeff10 years ago91onmessage: null,
92postMessageArgument: null
4677921cdigeff10 years ago93};
94this.sandbox.self = this.sandbox;
95
96this.sandboxContext = vm.createContext(this.sandbox);
97}
98
b3a793eeNisheet Jain10 years ago99private runInSandbox(filename: string, fileContents?: string): Q.Promise<void> {
100let fileContentsPromise = fileContents
101? Q(fileContents)
102: this.readFileContents(filename);
103
104return fileContentsPromise.then(contents => {
105vm.runInContext(contents, this.sandboxContext, filename);
106});
107}
108
109private readFileContents(filename: string) {
110return new Node.FileSystem().readFile(filename).then(contents => contents.toString());
111}
112
4677921cdigeff10 years ago113private importScripts(url: string): void {
5d4d4de0digeff10 years ago114/* The debuggerWorker.js executes this code:
115importScripts(message.url);
116sendReply();
117
118In 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
119actually send the reply back to the application until after importScripts has finished executing. We use
120this.pendingScriptImport to make the gotResponseFromDebuggerWorker() method hold the reply back, until've finished importing
121and running the script */
4677921cdigeff10 years ago122let defer = Q.defer<{}>();
5d4d4de0digeff10 years ago123this.pendingScriptImport = defer.promise;
4677921cdigeff10 years ago124
125// The next line converts to any due to the incorrect typing on node.d.ts of vm.runInThisContext
2743f19cdlebu10 years ago126new ScriptImporter(this.sourcesStoragePath).downloadAppScript(url, this.debugAdapterPort)
4677921cdigeff10 years ago127.then(downloadedScript =>
128this.runInSandbox(downloadedScript.filepath, downloadedScript.contents))
5d4d4de0digeff10 years ago129.done(() => {
9d7db611digeff10 years ago130// Now we let the reply to the app proceed
131defer.resolve({});
132}, reason => {
ce591c62digeff10 years ago133printDebuggingError(`Couldn't import script at <${url}>`, reason);
9d7db611digeff10 years ago134});
4677921cdigeff10 years ago135}
136
137private gotResponseFromDebuggerWorker(object: any): void {
5d4d4de0digeff10 years ago138// We might need to hold the response until a script is imported. See comments on this.importScripts()
139this.pendingScriptImport.done(() =>
10873e11digeff10 years ago140this.postReplyToApp(object), reason => {
ce591c62digeff10 years ago141printDebuggingError("Unexpected internal error while processing a message from the RN App.", reason);
10873e11digeff10 years ago142});
4677921cdigeff10 years ago143}
144}
145
146export class MultipleLifetimesAppWorker {
ea8a5f88digeff10 years ago147/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
148* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
149* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created succesfully.
150* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
151*/
4677921cdigeff10 years ago152private sourcesStoragePath: string;
5547a16fJimmy Thomson10 years ago153private debugAdapterPort: number;
ea8a5f88digeff10 years ago154private socketToApp: WebSocket;
4677921cdigeff10 years ago155private singleLifetimeWorker: SandboxedAppWorker;
156
5547a16fJimmy Thomson10 years ago157constructor(sourcesStoragePath: string, debugAdapterPort: number) {
4677921cdigeff10 years ago158this.sourcesStoragePath = sourcesStoragePath;
5547a16fJimmy Thomson10 years ago159this.debugAdapterPort = debugAdapterPort;
ea8a5f88digeff10 years ago160console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
4677921cdigeff10 years ago161}
162
5d4d4de0digeff10 years ago163public start(): Q.Promise<void> {
5547a16fJimmy Thomson10 years ago164this.singleLifetimeWorker = new SandboxedAppWorker(this.sourcesStoragePath, this.debugAdapterPort, (message) => {
5d4d4de0digeff10 years ago165this.sendMessageToApp(message);
166});
167return this.singleLifetimeWorker.start().then(() => {
168this.socketToApp = this.createSocketToApp();
169});
4677921cdigeff10 years ago170}
171
172private createSocketToApp() {
173let socketToApp = new WebSocket(this.debuggerProxyUrl());
ea8a5f88digeff10 years ago174socketToApp.on("open", () =>
175this.onSocketOpened());
176socketToApp.on("close", () =>
177this.onSocketClose());
178socketToApp.on("message",
179(message: any) => this.onMessage(message));
180socketToApp.on("error",
ce591c62digeff10 years ago181(error: Error) => printDebuggingError("An error ocurred while using the socket to communicate with the React Native app", error));
4677921cdigeff10 years ago182return socketToApp;
183}
184
185private debuggerProxyUrl() {
f158bbd1Atticus White10 years ago186return `ws://${Packager.HOST}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago187}
188
ea8a5f88digeff10 years ago189private onSocketOpened() {
4677921cdigeff10 years ago190Log.logMessage("Established a connection with the Proxy (Packager) to the React Native application");
191}
192
ea8a5f88digeff10 years ago193private onSocketClose() {
5d4d4de0digeff10 years ago194// TODO: Add some logic to not print this message that often, we'll spam the user
4677921cdigeff10 years ago195Log.logMessage("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...");
5d4d4de0digeff10 years ago196setTimeout(() => this.start(), 100);
4677921cdigeff10 years ago197}
198
ea8a5f88digeff10 years ago199private onMessage(message: string) {
5d4d4de0digeff10 years ago200try {
ea8a5f88digeff10 years ago201Log.logInternalMessage(LogLevel.Trace, "From RN APP: " + message);
202let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago203if (object.method === "prepareJSRuntime") {
204// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
205this.gotPrepareJSRuntime(object);
206} else if (object.method) {
207// All the other messages are handled by the single lifetime worker
208this.singleLifetimeWorker.postMessage(object);
209} else {
ea8a5f88digeff10 years ago210// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
211Log.logInternalMessage(LogLevel.Info, "The react-native app sent a message without specifying a method: " + message);
5d4d4de0digeff10 years ago212}
213} catch (exception) {
ce591c62digeff10 years ago214printDebuggingError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
4677921cdigeff10 years ago215}
216}
217
218private gotPrepareJSRuntime(message: any): void {
219// Create the sandbox, and replay that we finished processing the message
220this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
221}
222
223private sendMessageToApp(message: any) {
354c28a1digeff10 years ago224let stringified: string = null;
225try {
226stringified = JSON.stringify(message);
227Log.logInternalMessage(LogLevel.Trace, "To RN APP: " + stringified);
228this.socketToApp.send(stringified);
229} catch (exception) {
230let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
ce591c62digeff10 years ago231printDebuggingError(`Failed to send message to the React Native app. Message:\n${messageToShow}`, exception);
354c28a1digeff10 years ago232}
4677921cdigeff10 years ago233}
234}