microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
280c07463f45573fc0cd19fc2d2eb91eb3e828fd

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

310lines · 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 {
bf32340cJimmy Thomson10 years ago20__debug__: {
a39f743aJimmy Thomson10 years ago21// To support simulating native functionality when debugging,
22// we expose a node require function to the app
bf32340cJimmy Thomson10 years ago23require: (id: string) => any;
24};
4677921cdigeff10 years ago25__filename: string;
26__dirname: string;
27self: DebuggerWorkerSandbox;
ea8a5f88digeff10 years ago28console: typeof console;
29require: (id: string) => any;
4677921cdigeff10 years ago30importScripts: (url: string) => void;
31postMessage: (object: any) => void;
ea8a5f88digeff10 years ago32onmessage: (object: RNAppMessage) => void;
33postMessageArgument: RNAppMessage; // We use this argument to pass messages to the worker
4677921cdigeff10 years ago34}
35
ea8a5f88digeff10 years ago36interface RNAppMessage {
37method: string;
38// These objects have also other properties but that we don't currently use
39}
5d4d4de0digeff10 years ago40
ce591c62digeff10 years ago41function printDebuggingError(message: string, reason: any) {
a4a7e387Meena Kunnathur Balakrishnan10 years ago42Log.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 ago43}
44
4677921cdigeff10 years ago45export class SandboxedAppWorker {
ea8a5f88digeff10 years ago46/** This class will run the RN App logic inside a sandbox. The framework to run the logic is provided by the file
47* debuggerWorker.js (designed to run on a WebWorker). We load that file inside a sandbox, and then we use the
48* PROCESS_MESSAGE_INSIDE_SANDBOX script to execute the logic to respond to a message inside the sandbox.
49* The code inside the debuggerWorker.js will call the global function postMessage to send a reply back to the app,
50* so we define our custom function there, so we can handle the message. We also provide our own importScript function
51* to download any script used by debuggerWorker.js
52*/
e3ae4227digeff10 years ago53private packagerPort: number;
4677921cdigeff10 years ago54private sourcesStoragePath: string;
5547a16fJimmy Thomson10 years ago55private debugAdapterPort: number;
4677921cdigeff10 years ago56private postReplyToApp: (message: any) => void;
57
58private sandbox: DebuggerWorkerSandbox;
59private sandboxContext: vm.Context;
ea8a5f88digeff10 years ago60private scriptToReceiveMessageInSandbox: vm.Script;
4677921cdigeff10 years ago61
5d4d4de0digeff10 years ago62private pendingScriptImport = Q(void 0);
4677921cdigeff10 years ago63
3b6023b2Jimmy Thomson10 years ago64private nodeFileSystem: FileSystem;
65private scriptImporter: ScriptImporter;
66
ea8a5f88digeff10 years ago67private static PROCESS_MESSAGE_INSIDE_SANDBOX = "onmessage({ data: postMessageArgument });";
68
e3ae4227digeff10 years ago69constructor(packagerPort: number, sourcesStoragePath: string, debugAdapterPort: number, postReplyToApp: (message: any) => void, {
3b6023b2Jimmy Thomson10 years ago70nodeFileSystem = new FileSystem(),
5e651f3edigeff10 years ago71scriptImporter = new ScriptImporter(packagerPort, sourcesStoragePath),
3b6023b2Jimmy Thomson10 years ago72} = {}) {
e3ae4227digeff10 years ago73this.packagerPort = packagerPort;
4677921cdigeff10 years ago74this.sourcesStoragePath = sourcesStoragePath;
2570720bJimmy Thomson10 years ago75this.debugAdapterPort = debugAdapterPort;
4677921cdigeff10 years ago76this.postReplyToApp = postReplyToApp;
ea8a5f88digeff10 years ago77this.scriptToReceiveMessageInSandbox = new vm.Script(SandboxedAppWorker.PROCESS_MESSAGE_INSIDE_SANDBOX);
3b6023b2Jimmy Thomson10 years ago78
79this.nodeFileSystem = nodeFileSystem;
80this.scriptImporter = scriptImporter;
4677921cdigeff10 years ago81}
82
5d4d4de0digeff10 years ago83public start(): Q.Promise<void> {
2743f19cdlebu10 years ago84let scriptToRunPath = require.resolve(path.join(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILE_BASENAME));
4677921cdigeff10 years ago85this.initializeSandboxAndContext(scriptToRunPath);
5d4d4de0digeff10 years ago86return this.readFileContents(scriptToRunPath).then(fileContents =>
87// On a debugger worker the onmessage variable already exist. We need to declare it before the
88// javascript file can assign it. We do it in the first line without a new line to not break
89// the debugging experience of debugging debuggerWorker.js itself (as part of the extension)
90this.runInSandbox(scriptToRunPath, "var onmessage = null; " + fileContents));
4677921cdigeff10 years ago91}
92
ea8a5f88digeff10 years ago93public postMessage(object: RNAppMessage): void {
94this.sandbox.postMessageArgument = object;
95this.scriptToReceiveMessageInSandbox.runInContext(this.sandboxContext);
4677921cdigeff10 years ago96}
97
98private initializeSandboxAndContext(scriptToRunPath: string): void {
99let scriptToRunModule = new Module(scriptToRunPath);
bf32340cJimmy Thomson10 years ago100scriptToRunModule.paths = Module._nodeModulePaths(path.dirname(scriptToRunPath));
a39f743aJimmy Thomson10 years ago101// In order for __debug_.require("aNonInternalPackage") to work, we need to initialize where
102// node searches for packages. We invoke the same method that node does:
103// https://github.com/nodejs/node/blob/de1dc0ae2eb52842b5c5c974090123a64c3a594c/lib/module.js#L452
4677921cdigeff10 years ago104
105this.sandbox = {
bf32340cJimmy Thomson10 years ago106__debug__: {
107require: (filePath: string) => scriptToRunModule.require(filePath),
108},
4677921cdigeff10 years ago109__filename: scriptToRunPath,
110__dirname: path.dirname(scriptToRunPath),
111self: null,
112console: console,
113require: (filePath: string) => scriptToRunModule.require(filePath), // Give the sandbox access to require("<filePath>");
114importScripts: (url: string) => this.importScripts(url), // Import script like using <script/>
115postMessage: (object: any) => this.gotResponseFromDebuggerWorker(object), // Post message back to the UI thread
ea8a5f88digeff10 years ago116onmessage: null,
cdf34447digeff10 years ago117postMessageArgument: null,
4677921cdigeff10 years ago118};
119this.sandbox.self = this.sandbox;
120
121this.sandboxContext = vm.createContext(this.sandbox);
122}
123
b3a793eeNisheet Jain10 years ago124private runInSandbox(filename: string, fileContents?: string): Q.Promise<void> {
125let fileContentsPromise = fileContents
126? Q(fileContents)
127: this.readFileContents(filename);
128
129return fileContentsPromise.then(contents => {
130vm.runInContext(contents, this.sandboxContext, filename);
131});
132}
133
134private readFileContents(filename: string) {
3b6023b2Jimmy Thomson10 years ago135return this.nodeFileSystem.readFile(filename).then(contents => contents.toString());
b3a793eeNisheet Jain10 years ago136}
137
4677921cdigeff10 years ago138private importScripts(url: string): void {
5d4d4de0digeff10 years ago139/* The debuggerWorker.js executes this code:
140importScripts(message.url);
141sendReply();
142
143In 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
144actually send the reply back to the application until after importScripts has finished executing. We use
145this.pendingScriptImport to make the gotResponseFromDebuggerWorker() method hold the reply back, until've finished importing
146and running the script */
4677921cdigeff10 years ago147let defer = Q.defer<{}>();
5d4d4de0digeff10 years ago148this.pendingScriptImport = defer.promise;
4677921cdigeff10 years ago149
150// The next line converts to any due to the incorrect typing on node.d.ts of vm.runInThisContext
3b6023b2Jimmy Thomson10 years ago151this.scriptImporter.downloadAppScript(url, this.debugAdapterPort)
4677921cdigeff10 years ago152.then(downloadedScript =>
153this.runInSandbox(downloadedScript.filepath, downloadedScript.contents))
5d4d4de0digeff10 years ago154.done(() => {
9d7db611digeff10 years ago155// Now we let the reply to the app proceed
156defer.resolve({});
157}, reason => {
ce591c62digeff10 years ago158printDebuggingError(`Couldn't import script at <${url}>`, reason);
9d7db611digeff10 years ago159});
4677921cdigeff10 years ago160}
161
162private gotResponseFromDebuggerWorker(object: any): void {
5d4d4de0digeff10 years ago163// We might need to hold the response until a script is imported. See comments on this.importScripts()
164this.pendingScriptImport.done(() =>
10873e11digeff10 years ago165this.postReplyToApp(object), reason => {
ce591c62digeff10 years ago166printDebuggingError("Unexpected internal error while processing a message from the RN App.", reason);
10873e11digeff10 years ago167});
4677921cdigeff10 years ago168}
169}
170
171export class MultipleLifetimesAppWorker {
ea8a5f88digeff10 years ago172/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
173* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
80002087Joshua Skelton10 years ago174* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
ea8a5f88digeff10 years ago175* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
176*/
e3ae4227digeff10 years ago177private packagerPort: number;
4677921cdigeff10 years ago178private sourcesStoragePath: string;
5547a16fJimmy Thomson10 years ago179private debugAdapterPort: number;
ea8a5f88digeff10 years ago180private socketToApp: WebSocket;
4677921cdigeff10 years ago181private singleLifetimeWorker: SandboxedAppWorker;
182
3b6023b2Jimmy Thomson10 years ago183private sandboxedAppConstructor: (storagePath: string, adapterPort: number, messageFunction: (message: any) => void) => SandboxedAppWorker;
184private webSocketConstructor: (url: string) => WebSocket;
185
7cc67271digeff10 years ago186private executionLimiter = new ExecutionsLimiter();
187
e3ae4227digeff10 years ago188constructor(packagerPort: number, sourcesStoragePath: string, debugAdapterPort: number, {
5e651f3edigeff10 years ago189sandboxedAppConstructor = (path: string, port: number, messageFunc: (message: any) => void) =>
190new SandboxedAppWorker(packagerPort, path, port, messageFunc),
cdf34447digeff10 years ago191webSocketConstructor = (url: string) => new WebSocket(url),
3b6023b2Jimmy Thomson10 years ago192} = {}) {
e3ae4227digeff10 years ago193this.packagerPort = packagerPort;
4677921cdigeff10 years ago194this.sourcesStoragePath = sourcesStoragePath;
5547a16fJimmy Thomson10 years ago195this.debugAdapterPort = debugAdapterPort;
ea8a5f88digeff10 years ago196console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
3b6023b2Jimmy Thomson10 years ago197
198this.sandboxedAppConstructor = sandboxedAppConstructor;
199this.webSocketConstructor = webSocketConstructor;
4677921cdigeff10 years ago200}
201
b9356af0Meena Kunnathur Balakrishnan10 years ago202public start(warnOnFailure: boolean = false): Q.Promise<any> {
299b0557Patricio Beltran10 years ago203return Packager.isPackagerRunning(Packager.getHostForPort(this.packagerPort))
204.then(running => {
205if (running) {
206return this.createSocketToApp(warnOnFailure);
207}
208throw new Error(`Cannot attach to packager. Are you sure there is a packager and it is running in the port ${this.packagerPort}? If your packager is configured to run in another port make sure to add that to the setting.json.`);
209});
ff7dce65digeff10 years ago210}
211
212private startNewWorkerLifetime(): Q.Promise<void> {
3b6023b2Jimmy Thomson10 years ago213this.singleLifetimeWorker = this.sandboxedAppConstructor(this.sourcesStoragePath, this.debugAdapterPort, (message) => {
5d4d4de0digeff10 years ago214this.sendMessageToApp(message);
215});
ff7dce65digeff10 years ago216Log.logInternalMessage(LogLevel.Info, "A new app worker lifetime was created.");
217return this.singleLifetimeWorker.start();
4677921cdigeff10 years ago218}
219
b9356af0Meena Kunnathur Balakrishnan10 years ago220private createSocketToApp(warnOnFailure: boolean = false): Q.Promise<void> {
221let deferred = Q.defer<void>();
6e731058Jimmy Thomson10 years ago222this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
b9356af0Meena Kunnathur Balakrishnan10 years ago223this.socketToApp.on("open", () => {
224this.onSocketOpened();
225});
299b0557Patricio Beltran10 years ago226this.socketToApp.on("close",
227() => {
228this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
229/*
230* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
231* it closes the socket because it already has a connection to a debugger.
232* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
233*/
234if (this.socketToApp._closeMessage === "Another debugger is already connected") {
235deferred.reject(new RangeError("Another debugger is already connected to packager. Please close it before trying to debug with VSCode."));
236}
237Log.logMessage("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...");
238});
239setTimeout(() => {
240this.start(true /* retryAttempt */);
241}, 100);
242});
b9356af0Meena Kunnathur Balakrishnan10 years ago243this.socketToApp.on("message",
ea8a5f88digeff10 years ago244(message: any) => this.onMessage(message));
b9356af0Meena Kunnathur Balakrishnan10 years ago245this.socketToApp.on("error",
32cab018Meena Kunnathur Balakrishnan10 years ago246(error: Error) => {
b9356af0Meena Kunnathur Balakrishnan10 years ago247if (warnOnFailure) {
248Log.logWarning(ErrorHelper.getNestedWarning(error,
249"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 ago250}
251
32cab018Meena Kunnathur Balakrishnan10 years ago252deferred.reject(error);
253});
254
499fe4ebMeena Kunnathur Balakrishnan10 years ago255// In an attempt to catch failures in starting the packager on first attempt,
256// wait for 300 ms before resolving the promise
b9356af0Meena Kunnathur Balakrishnan10 years ago257Q.delay(300).done(() => deferred.resolve(void 0));
32cab018Meena Kunnathur Balakrishnan10 years ago258return deferred.promise;
4677921cdigeff10 years ago259}
260
261private debuggerProxyUrl() {
5e651f3edigeff10 years ago262return `ws://${Packager.getHostForPort(this.packagerPort)}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago263}
264
ea8a5f88digeff10 years ago265private onSocketOpened() {
7cc67271digeff10 years ago266this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
267Log.logMessage("Established a connection with the Proxy (Packager) to the React Native application"));
4677921cdigeff10 years ago268}
269
ea8a5f88digeff10 years ago270private onMessage(message: string) {
5d4d4de0digeff10 years ago271try {
ea8a5f88digeff10 years ago272Log.logInternalMessage(LogLevel.Trace, "From RN APP: " + message);
273let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago274if (object.method === "prepareJSRuntime") {
275// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
276this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago277} else if (object.method === "$disconnected") {
278// We need to shutdown the current app worker, and create a new lifetime
279this.singleLifetimeWorker = null;
5d4d4de0digeff10 years ago280} else if (object.method) {
281// All the other messages are handled by the single lifetime worker
282this.singleLifetimeWorker.postMessage(object);
283} else {
ea8a5f88digeff10 years ago284// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
285Log.logInternalMessage(LogLevel.Info, "The react-native app sent a message without specifying a method: " + message);
5d4d4de0digeff10 years ago286}
287} catch (exception) {
ce591c62digeff10 years ago288printDebuggingError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
4677921cdigeff10 years ago289}
290}
291
292private gotPrepareJSRuntime(message: any): void {
293// Create the sandbox, and replay that we finished processing the message
ff7dce65digeff10 years ago294this.startNewWorkerLifetime().done(() => {
295this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
296}, error => printDebuggingError(`Failed to prepare the JavaScript runtime environment. Message:\n${message}`, error));
4677921cdigeff10 years ago297}
298
ff7dce65digeff10 years ago299private sendMessageToApp(message: any): void {
354c28a1digeff10 years ago300let stringified: string = null;
301try {
302stringified = JSON.stringify(message);
303Log.logInternalMessage(LogLevel.Trace, "To RN APP: " + stringified);
304this.socketToApp.send(stringified);
305} catch (exception) {
306let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
ce591c62digeff10 years ago307printDebuggingError(`Failed to send message to the React Native app. Message:\n${messageToShow}`, exception);
354c28a1digeff10 years ago308}
4677921cdigeff10 years ago309}
310}