microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.1.4

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

278lines · 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";
a4a7e387Meena Kunnathur Balakrishnan10 years ago10import {ErrorHelper} from "../common/error/errorHelper";
190e393cMeena Kunnathur Balakrishnan10 years ago11import {Log} from "../common/log/log";
12import {LogLevel} from "../common/log/logHelper";
3b6023b2Jimmy Thomson10 years ago13import {FileSystem} from "../common/node/fileSystem";
7cc67271digeff10 years ago14import {ExecutionsLimiter} from "../common/executionsLimiter";
4677921cdigeff10 years ago15
16import Module = require("module");
17
18// This file is a replacement of: https://github.com/facebook/react-native/blob/8d397b4cbc05ad801cfafb421cee39bcfe89711d/local-cli/server/util/debugger.html for Node.JS
19interface DebuggerWorkerSandbox {
20__filename: string;
21__dirname: string;
22self: DebuggerWorkerSandbox;
ea8a5f88digeff10 years ago23console: typeof console;
24require: (id: string) => any;
4677921cdigeff10 years ago25importScripts: (url: string) => void;
26postMessage: (object: any) => void;
ea8a5f88digeff10 years ago27onmessage: (object: RNAppMessage) => void;
28postMessageArgument: RNAppMessage; // We use this argument to pass messages to the worker
4677921cdigeff10 years ago29}
30
ea8a5f88digeff10 years ago31interface RNAppMessage {
32method: string;
33// These objects have also other properties but that we don't currently use
34}
5d4d4de0digeff10 years ago35
ce591c62digeff10 years ago36function printDebuggingError(message: string, reason: any) {
a4a7e387Meena Kunnathur Balakrishnan10 years ago37Log.logWarning(ErrorHelper.getNestedWarning(reason, `${message}. Debugging won't work: Try reloading the JS from inside the app, or Reconnect the VS Code debugger`));
5d4d4de0digeff10 years ago38}
39
4677921cdigeff10 years ago40export class SandboxedAppWorker {
ea8a5f88digeff10 years ago41/** This class will run the RN App logic inside a sandbox. The framework to run the logic is provided by the file
42* debuggerWorker.js (designed to run on a WebWorker). We load that file inside a sandbox, and then we use the
43* PROCESS_MESSAGE_INSIDE_SANDBOX script to execute the logic to respond to a message inside the sandbox.
44* The code inside the debuggerWorker.js will call the global function postMessage to send a reply back to the app,
45* so we define our custom function there, so we can handle the message. We also provide our own importScript function
46* to download any script used by debuggerWorker.js
47*/
4677921cdigeff10 years ago48private sourcesStoragePath: string;
5547a16fJimmy Thomson10 years ago49private debugAdapterPort: number;
4677921cdigeff10 years ago50private postReplyToApp: (message: any) => void;
51
52private sandbox: DebuggerWorkerSandbox;
53private sandboxContext: vm.Context;
ea8a5f88digeff10 years ago54private scriptToReceiveMessageInSandbox: vm.Script;
4677921cdigeff10 years ago55
5d4d4de0digeff10 years ago56private pendingScriptImport = Q(void 0);
4677921cdigeff10 years ago57
3b6023b2Jimmy Thomson10 years ago58private nodeFileSystem: FileSystem;
59private scriptImporter: ScriptImporter;
60
ea8a5f88digeff10 years ago61private static PROCESS_MESSAGE_INSIDE_SANDBOX = "onmessage({ data: postMessageArgument });";
62
3b6023b2Jimmy Thomson10 years ago63constructor(sourcesStoragePath: string, debugAdapterPort: number, postReplyToApp: (message: any) => void, {
64nodeFileSystem = new FileSystem(),
cdf34447digeff10 years ago65scriptImporter = new ScriptImporter(sourcesStoragePath),
3b6023b2Jimmy Thomson10 years ago66} = {}) {
4677921cdigeff10 years ago67this.sourcesStoragePath = sourcesStoragePath;
2570720bJimmy Thomson10 years ago68this.debugAdapterPort = debugAdapterPort;
4677921cdigeff10 years ago69this.postReplyToApp = postReplyToApp;
ea8a5f88digeff10 years ago70this.scriptToReceiveMessageInSandbox = new vm.Script(SandboxedAppWorker.PROCESS_MESSAGE_INSIDE_SANDBOX);
3b6023b2Jimmy Thomson10 years ago71
72this.nodeFileSystem = nodeFileSystem;
73this.scriptImporter = scriptImporter;
4677921cdigeff10 years ago74}
75
5d4d4de0digeff10 years ago76public start(): Q.Promise<void> {
2743f19cdlebu10 years ago77let scriptToRunPath = require.resolve(path.join(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILE_BASENAME));
4677921cdigeff10 years ago78this.initializeSandboxAndContext(scriptToRunPath);
5d4d4de0digeff10 years ago79return this.readFileContents(scriptToRunPath).then(fileContents =>
80// On a debugger worker the onmessage variable already exist. We need to declare it before the
81// javascript file can assign it. We do it in the first line without a new line to not break
82// the debugging experience of debugging debuggerWorker.js itself (as part of the extension)
83this.runInSandbox(scriptToRunPath, "var onmessage = null; " + fileContents));
4677921cdigeff10 years ago84}
85
ea8a5f88digeff10 years ago86public postMessage(object: RNAppMessage): void {
87this.sandbox.postMessageArgument = object;
88this.scriptToReceiveMessageInSandbox.runInContext(this.sandboxContext);
4677921cdigeff10 years ago89}
90
91private initializeSandboxAndContext(scriptToRunPath: string): void {
92let scriptToRunModule = new Module(scriptToRunPath);
93
94this.sandbox = {
95__filename: scriptToRunPath,
96__dirname: path.dirname(scriptToRunPath),
97self: null,
98console: console,
99require: (filePath: string) => scriptToRunModule.require(filePath), // Give the sandbox access to require("<filePath>");
100importScripts: (url: string) => this.importScripts(url), // Import script like using <script/>
101postMessage: (object: any) => this.gotResponseFromDebuggerWorker(object), // Post message back to the UI thread
ea8a5f88digeff10 years ago102onmessage: null,
cdf34447digeff10 years ago103postMessageArgument: null,
4677921cdigeff10 years ago104};
105this.sandbox.self = this.sandbox;
106
107this.sandboxContext = vm.createContext(this.sandbox);
108}
109
b3a793eeNisheet Jain10 years ago110private runInSandbox(filename: string, fileContents?: string): Q.Promise<void> {
111let fileContentsPromise = fileContents
112? Q(fileContents)
113: this.readFileContents(filename);
114
115return fileContentsPromise.then(contents => {
116vm.runInContext(contents, this.sandboxContext, filename);
117});
118}
119
120private readFileContents(filename: string) {
3b6023b2Jimmy Thomson10 years ago121return this.nodeFileSystem.readFile(filename).then(contents => contents.toString());
b3a793eeNisheet Jain10 years ago122}
123
4677921cdigeff10 years ago124private importScripts(url: string): void {
5d4d4de0digeff10 years ago125/* The debuggerWorker.js executes this code:
126importScripts(message.url);
127sendReply();
128
129In 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
130actually send the reply back to the application until after importScripts has finished executing. We use
131this.pendingScriptImport to make the gotResponseFromDebuggerWorker() method hold the reply back, until've finished importing
132and running the script */
4677921cdigeff10 years ago133let defer = Q.defer<{}>();
5d4d4de0digeff10 years ago134this.pendingScriptImport = defer.promise;
4677921cdigeff10 years ago135
136// The next line converts to any due to the incorrect typing on node.d.ts of vm.runInThisContext
3b6023b2Jimmy Thomson10 years ago137this.scriptImporter.downloadAppScript(url, this.debugAdapterPort)
4677921cdigeff10 years ago138.then(downloadedScript =>
139this.runInSandbox(downloadedScript.filepath, downloadedScript.contents))
5d4d4de0digeff10 years ago140.done(() => {
9d7db611digeff10 years ago141// Now we let the reply to the app proceed
142defer.resolve({});
143}, reason => {
ce591c62digeff10 years ago144printDebuggingError(`Couldn't import script at <${url}>`, reason);
9d7db611digeff10 years ago145});
4677921cdigeff10 years ago146}
147
148private gotResponseFromDebuggerWorker(object: any): void {
5d4d4de0digeff10 years ago149// We might need to hold the response until a script is imported. See comments on this.importScripts()
150this.pendingScriptImport.done(() =>
10873e11digeff10 years ago151this.postReplyToApp(object), reason => {
ce591c62digeff10 years ago152printDebuggingError("Unexpected internal error while processing a message from the RN App.", reason);
10873e11digeff10 years ago153});
4677921cdigeff10 years ago154}
155}
156
157export class MultipleLifetimesAppWorker {
ea8a5f88digeff10 years ago158/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
159* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
80002087Joshua Skelton10 years ago160* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
ea8a5f88digeff10 years ago161* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
162*/
4677921cdigeff10 years ago163private sourcesStoragePath: string;
5547a16fJimmy Thomson10 years ago164private debugAdapterPort: number;
ea8a5f88digeff10 years ago165private socketToApp: WebSocket;
4677921cdigeff10 years ago166private singleLifetimeWorker: SandboxedAppWorker;
167
3b6023b2Jimmy Thomson10 years ago168private sandboxedAppConstructor: (storagePath: string, adapterPort: number, messageFunction: (message: any) => void) => SandboxedAppWorker;
169private webSocketConstructor: (url: string) => WebSocket;
170
7cc67271digeff10 years ago171private executionLimiter = new ExecutionsLimiter();
172
3b6023b2Jimmy Thomson10 years ago173constructor(sourcesStoragePath: string, debugAdapterPort: number, {
174sandboxedAppConstructor = (path: string, port: number, messageFunc: (message: any) => void) => new SandboxedAppWorker(path, port, messageFunc),
cdf34447digeff10 years ago175webSocketConstructor = (url: string) => new WebSocket(url),
3b6023b2Jimmy Thomson10 years ago176} = {}) {
4677921cdigeff10 years ago177this.sourcesStoragePath = sourcesStoragePath;
5547a16fJimmy Thomson10 years ago178this.debugAdapterPort = debugAdapterPort;
ea8a5f88digeff10 years ago179console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
3b6023b2Jimmy Thomson10 years ago180
181this.sandboxedAppConstructor = sandboxedAppConstructor;
182this.webSocketConstructor = webSocketConstructor;
4677921cdigeff10 years ago183}
184
b9356af0Meena Kunnathur Balakrishnan10 years ago185public start(warnOnFailure: boolean = false): Q.Promise<any> {
186return this.createSocketToApp(warnOnFailure);
ff7dce65digeff10 years ago187}
188
189private startNewWorkerLifetime(): Q.Promise<void> {
3b6023b2Jimmy Thomson10 years ago190this.singleLifetimeWorker = this.sandboxedAppConstructor(this.sourcesStoragePath, this.debugAdapterPort, (message) => {
5d4d4de0digeff10 years ago191this.sendMessageToApp(message);
192});
ff7dce65digeff10 years ago193Log.logInternalMessage(LogLevel.Info, "A new app worker lifetime was created.");
194return this.singleLifetimeWorker.start();
4677921cdigeff10 years ago195}
196
b9356af0Meena Kunnathur Balakrishnan10 years ago197private createSocketToApp(warnOnFailure: boolean = false): Q.Promise<void> {
198let deferred = Q.defer<void>();
6e731058Jimmy Thomson10 years ago199this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
b9356af0Meena Kunnathur Balakrishnan10 years ago200this.socketToApp.on("open", () => {
201this.onSocketOpened();
202});
203this.socketToApp.on("close", () =>
ea8a5f88digeff10 years ago204this.onSocketClose());
b9356af0Meena Kunnathur Balakrishnan10 years ago205this.socketToApp.on("message",
ea8a5f88digeff10 years ago206(message: any) => this.onMessage(message));
b9356af0Meena Kunnathur Balakrishnan10 years ago207this.socketToApp.on("error",
32cab018Meena Kunnathur Balakrishnan10 years ago208(error: Error) => {
b9356af0Meena Kunnathur Balakrishnan10 years ago209if (warnOnFailure) {
210Log.logWarning(ErrorHelper.getNestedWarning(error,
211"Reconnection to the proxy (Packager) failed. Please check the output window for Packager errors, if any. If failure persists, please restart the React Native debugger."));
499fe4ebMeena Kunnathur Balakrishnan10 years ago212}
213
32cab018Meena Kunnathur Balakrishnan10 years ago214deferred.reject(error);
215});
216
499fe4ebMeena Kunnathur Balakrishnan10 years ago217// In an attempt to catch failures in starting the packager on first attempt,
218// wait for 300 ms before resolving the promise
b9356af0Meena Kunnathur Balakrishnan10 years ago219Q.delay(300).done(() => deferred.resolve(void 0));
32cab018Meena Kunnathur Balakrishnan10 years ago220return deferred.promise;
4677921cdigeff10 years ago221}
222
223private debuggerProxyUrl() {
f158bbd1Atticus White10 years ago224return `ws://${Packager.HOST}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago225}
226
ea8a5f88digeff10 years ago227private onSocketOpened() {
7cc67271digeff10 years ago228this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
229Log.logMessage("Established a connection with the Proxy (Packager) to the React Native application"));
4677921cdigeff10 years ago230}
231
ea8a5f88digeff10 years ago232private onSocketClose() {
7cc67271digeff10 years ago233this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () =>
234Log.logMessage("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon..."));
499fe4ebMeena Kunnathur Balakrishnan10 years ago235setTimeout(() => this.start(true /* retryAttempt */), 100);
4677921cdigeff10 years ago236}
237
ea8a5f88digeff10 years ago238private onMessage(message: string) {
5d4d4de0digeff10 years ago239try {
ea8a5f88digeff10 years ago240Log.logInternalMessage(LogLevel.Trace, "From RN APP: " + message);
241let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago242if (object.method === "prepareJSRuntime") {
243// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
244this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago245} else if (object.method === "$disconnected") {
246// We need to shutdown the current app worker, and create a new lifetime
247this.singleLifetimeWorker = null;
5d4d4de0digeff10 years ago248} else if (object.method) {
249// All the other messages are handled by the single lifetime worker
250this.singleLifetimeWorker.postMessage(object);
251} else {
ea8a5f88digeff10 years ago252// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
253Log.logInternalMessage(LogLevel.Info, "The react-native app sent a message without specifying a method: " + message);
5d4d4de0digeff10 years ago254}
255} catch (exception) {
ce591c62digeff10 years ago256printDebuggingError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
4677921cdigeff10 years ago257}
258}
259
260private gotPrepareJSRuntime(message: any): void {
261// Create the sandbox, and replay that we finished processing the message
ff7dce65digeff10 years ago262this.startNewWorkerLifetime().done(() => {
263this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
264}, error => printDebuggingError(`Failed to prepare the JavaScript runtime environment. Message:\n${message}`, error));
4677921cdigeff10 years ago265}
266
ff7dce65digeff10 years ago267private sendMessageToApp(message: any): void {
354c28a1digeff10 years ago268let stringified: string = null;
269try {
270stringified = JSON.stringify(message);
271Log.logInternalMessage(LogLevel.Trace, "To RN APP: " + stringified);
272this.socketToApp.send(stringified);
273} catch (exception) {
274let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
ce591c62digeff10 years ago275printDebuggingError(`Failed to send message to the React Native app. Message:\n${messageToShow}`, exception);
354c28a1digeff10 years ago276}
4677921cdigeff10 years ago277}
278}