microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.4.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

242lines · 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 Q from "q";
cc70057dVladimir Kotikov9 years ago5import * as path from "path";
ea8a5f88digeff10 years ago6import * as WebSocket from "ws";
e45838cbVladimir Kotikov9 years ago7import { EventEmitter } from "events";
b0061ac6Meena Kunnathur Balakrishnan10 years ago8import {Packager} from "../common/packager";
a4a7e387Meena Kunnathur Balakrishnan10 years ago9import {ErrorHelper} from "../common/error/errorHelper";
190e393cMeena Kunnathur Balakrishnan10 years ago10import {Log} from "../common/log/log";
11import {LogLevel} from "../common/log/logHelper";
7cc67271digeff10 years ago12import {ExecutionsLimiter} from "../common/executionsLimiter";
cc70057dVladimir Kotikov9 years ago13import { FileSystem as NodeFileSystem} from "../common/node/fileSystem";
b05b5086Vladimir Kotikov9 years ago14import { ForkedAppWorker } from "./forkedAppWorker";
cc70057dVladimir Kotikov9 years ago15import { ScriptImporter } from "./scriptImporter";
4677921cdigeff10 years ago16
e45838cbVladimir Kotikov9 years ago17export interface RNAppMessage {
ea8a5f88digeff10 years ago18method: string;
e45838cbVladimir Kotikov9 years ago19url?: string;
ea8a5f88digeff10 years ago20// These objects have also other properties but that we don't currently use
21}
5d4d4de0digeff10 years ago22
e45838cbVladimir Kotikov9 years ago23export interface IDebuggeeWorker {
24start(): Q.Promise<any>;
25stop(): void;
26postMessage(message: RNAppMessage): void;
27}
28
039239d1Vladimir Kotikov9 years ago29function printDebuggingError(message: string, reason: any) {
30Log.logWarning(ErrorHelper.getNestedWarning(reason, `${message}. Debugging won't work: Try reloading the JS from inside the app, or Reconnect the VS Code debugger`));
31}
32
33/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
34* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
35* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
36* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
37*/
38
39export class MultipleLifetimesAppWorker extends EventEmitter {
40public static WORKER_BOOTSTRAP = `
cc70057dVladimir Kotikov9 years ago41// Initialize some variables before react-native code would access them
d64a6928Vladimir Kotikov9 years ago42var onmessage=null, self=global;
cc70057dVladimir Kotikov9 years ago43// Cache Node's original require as __debug__.require
d64a6928Vladimir Kotikov9 years ago44global.__debug__={require: require};
45// avoid Node's GLOBAL deprecation warning
46Object.defineProperty(global, "GLOBAL", {
47configurable: true,
48writable: true,
49enumerable: true,
50value: global
51});
cc70057dVladimir Kotikov9 years ago52process.on("message", function(message){
53if (onmessage) onmessage(message);
54});
55var postMessage = function(message){
56process.send(message);
57};
58var importScripts = (function(){
59var fs=require('fs'), vm=require('vm');
60return function(scriptUrl){
61var scriptCode = fs.readFileSync(scriptUrl, "utf8");
62vm.runInThisContext(scriptCode, {filename: scriptUrl});
63};
64})();`;
65
039239d1Vladimir Kotikov9 years ago66public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago67// and started listening for IPC messages
68postMessage({workerLoaded:true});`;
69
e3ae4227digeff10 years ago70private packagerPort: number;
4677921cdigeff10 years ago71private sourcesStoragePath: string;
ea8a5f88digeff10 years ago72private socketToApp: WebSocket;
e45838cbVladimir Kotikov9 years ago73private singleLifetimeWorker: IDebuggeeWorker;
3b6023b2Jimmy Thomson10 years ago74private webSocketConstructor: (url: string) => WebSocket;
75
7cc67271digeff10 years ago76private executionLimiter = new ExecutionsLimiter();
cc70057dVladimir Kotikov9 years ago77private nodeFileSystem = new NodeFileSystem();
78private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago79
e45838cbVladimir Kotikov9 years ago80constructor(packagerPort: number, sourcesStoragePath: string, {
cdf34447digeff10 years ago81webSocketConstructor = (url: string) => new WebSocket(url),
3b6023b2Jimmy Thomson10 years ago82} = {}) {
e45838cbVladimir Kotikov9 years ago83super();
e3ae4227digeff10 years ago84this.packagerPort = packagerPort;
4677921cdigeff10 years ago85this.sourcesStoragePath = sourcesStoragePath;
ea8a5f88digeff10 years ago86console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
3b6023b2Jimmy Thomson10 years ago87
88this.webSocketConstructor = webSocketConstructor;
cc70057dVladimir Kotikov9 years ago89this.scriptImporter = new ScriptImporter(packagerPort, sourcesStoragePath);
4677921cdigeff10 years ago90}
91
cc70057dVladimir Kotikov9 years ago92public start(retryAttempt: boolean = false): Q.Promise<any> {
299b0557Patricio Beltran10 years ago93return Packager.isPackagerRunning(Packager.getHostForPort(this.packagerPort))
94.then(running => {
cc70057dVladimir Kotikov9 years ago95if (!running) {
96throw 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.`);
299b0557Patricio Beltran10 years ago97}
cc70057dVladimir Kotikov9 years ago98})
99.then(() => {
100// Don't fetch debugger worker on socket disconnect
101return retryAttempt ? Q.resolve<void>(void 0) :
102this.downloadAndPatchDebuggerWorker();
103})
104.then(() => this.createSocketToApp(retryAttempt));
ff7dce65digeff10 years ago105}
106
e45838cbVladimir Kotikov9 years ago107public stop() {
108if (this.socketToApp) {
109this.socketToApp.removeAllListeners();
110this.socketToApp.close();
111}
112
113if (this.singleLifetimeWorker) {
114this.singleLifetimeWorker.stop();
115}
116}
117
cc70057dVladimir Kotikov9 years ago118public downloadAndPatchDebuggerWorker(): Q.Promise<void> {
119let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
120return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath)
121.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
122.then((workerContent: string) => {
123// Add our customizations to debugger worker to get it working smoothly
124// in Node env and polyfill WebWorkers API over Node's IPC.
039239d1Vladimir Kotikov9 years ago125const modifiedDebuggeeContent = [MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
126workerContent, MultipleLifetimesAppWorker.WORKER_DONE].join("\n");
cc70057dVladimir Kotikov9 years ago127return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
128});
129}
130
ff7dce65digeff10 years ago131private startNewWorkerLifetime(): Q.Promise<void> {
b05b5086Vladimir Kotikov9 years ago132this.singleLifetimeWorker = new ForkedAppWorker(this.packagerPort, this.sourcesStoragePath, (message) => {
5d4d4de0digeff10 years ago133this.sendMessageToApp(message);
134});
ff7dce65digeff10 years ago135Log.logInternalMessage(LogLevel.Info, "A new app worker lifetime was created.");
e45838cbVladimir Kotikov9 years ago136return this.singleLifetimeWorker.start()
cc70057dVladimir Kotikov9 years ago137.then(startedEvent => {
138this.emit("connected", startedEvent);
139});
4677921cdigeff10 years ago140}
141
cc70057dVladimir Kotikov9 years ago142private createSocketToApp(retryAttempt: boolean = false): Q.Promise<void> {
b9356af0Meena Kunnathur Balakrishnan10 years ago143let deferred = Q.defer<void>();
6e731058Jimmy Thomson10 years ago144this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
b9356af0Meena Kunnathur Balakrishnan10 years ago145this.socketToApp.on("open", () => {
146this.onSocketOpened();
147});
299b0557Patricio Beltran10 years ago148this.socketToApp.on("close",
149() => {
150this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
151/*
152* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
153* it closes the socket because it already has a connection to a debugger.
154* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
155*/
6d5c8798Nikita Matrosov9 years ago156let msgKey = "_closeMessage";
157if (this.socketToApp[msgKey] === "Another debugger is already connected") {
299b0557Patricio Beltran10 years ago158deferred.reject(new RangeError("Another debugger is already connected to packager. Please close it before trying to debug with VSCode."));
159}
160Log.logMessage("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...");
161});
162setTimeout(() => {
163this.start(true /* retryAttempt */);
164}, 100);
165});
b9356af0Meena Kunnathur Balakrishnan10 years ago166this.socketToApp.on("message",
ea8a5f88digeff10 years ago167(message: any) => this.onMessage(message));
b9356af0Meena Kunnathur Balakrishnan10 years ago168this.socketToApp.on("error",
32cab018Meena Kunnathur Balakrishnan10 years ago169(error: Error) => {
cc70057dVladimir Kotikov9 years ago170if (retryAttempt) {
b9356af0Meena Kunnathur Balakrishnan10 years ago171Log.logWarning(ErrorHelper.getNestedWarning(error,
172"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 ago173}
174
32cab018Meena Kunnathur Balakrishnan10 years ago175deferred.reject(error);
176});
177
499fe4ebMeena Kunnathur Balakrishnan10 years ago178// In an attempt to catch failures in starting the packager on first attempt,
179// wait for 300 ms before resolving the promise
b9356af0Meena Kunnathur Balakrishnan10 years ago180Q.delay(300).done(() => deferred.resolve(void 0));
32cab018Meena Kunnathur Balakrishnan10 years ago181return deferred.promise;
4677921cdigeff10 years ago182}
183
184private debuggerProxyUrl() {
5e651f3edigeff10 years ago185return `ws://${Packager.getHostForPort(this.packagerPort)}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago186}
187
ea8a5f88digeff10 years ago188private onSocketOpened() {
7cc67271digeff10 years ago189this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
190Log.logMessage("Established a connection with the Proxy (Packager) to the React Native application"));
4677921cdigeff10 years ago191}
192
9174feb7Vladimir Kotikov9 years ago193private killWorker() {
194if (!this.singleLifetimeWorker) return;
195this.singleLifetimeWorker.stop();
196this.singleLifetimeWorker = null;
197}
e7b314e8Vladimir Kotikov9 years ago198
9174feb7Vladimir Kotikov9 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") {
e7b314e8Vladimir Kotikov9 years ago204// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
205// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago206this.killWorker();
5d4d4de0digeff10 years ago207// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
208this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago209} else if (object.method === "$disconnected") {
210// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago211this.killWorker();
5d4d4de0digeff10 years ago212} else if (object.method) {
213// All the other messages are handled by the single lifetime worker
214this.singleLifetimeWorker.postMessage(object);
215} else {
ea8a5f88digeff10 years ago216// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
217Log.logInternalMessage(LogLevel.Info, "The react-native app sent a message without specifying a method: " + message);
5d4d4de0digeff10 years ago218}
219} catch (exception) {
ce591c62digeff10 years ago220printDebuggingError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
4677921cdigeff10 years ago221}
222}
223
224private gotPrepareJSRuntime(message: any): void {
225// Create the sandbox, and replay that we finished processing the message
ff7dce65digeff10 years ago226this.startNewWorkerLifetime().done(() => {
227this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
228}, error => printDebuggingError(`Failed to prepare the JavaScript runtime environment. Message:\n${message}`, error));
4677921cdigeff10 years ago229}
230
ff7dce65digeff10 years ago231private sendMessageToApp(message: any): void {
354c28a1digeff10 years ago232let stringified: string = null;
233try {
234stringified = JSON.stringify(message);
235Log.logInternalMessage(LogLevel.Trace, "To RN APP: " + stringified);
236this.socketToApp.send(stringified);
237} catch (exception) {
238let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
ce591c62digeff10 years ago239printDebuggingError(`Failed to send message to the React Native app. Message:\n${messageToShow}`, exception);
354c28a1digeff10 years ago240}
4677921cdigeff10 years ago241}
242}