microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
14ebf4e6fdee5f69a41d9a7deea4dc164dd28b7e

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

311lines · 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";
0a68f8dbArtem Egorov8 years ago8import { ensurePackagerRunning } from "../common/packagerStatus";
a4a7e387Meena Kunnathur Balakrishnan10 years ago9import {ErrorHelper} from "../common/error/errorHelper";
0a68f8dbArtem Egorov8 years ago10import { logger } from "vscode-chrome-debug-core";
7cc67271digeff10 years ago11import {ExecutionsLimiter} from "../common/executionsLimiter";
cc70057dVladimir Kotikov9 years ago12import { FileSystem as NodeFileSystem} from "../common/node/fileSystem";
b05b5086Vladimir Kotikov9 years ago13import { ForkedAppWorker } from "./forkedAppWorker";
cc70057dVladimir Kotikov9 years ago14import { ScriptImporter } from "./scriptImporter";
bb869343Serge Svekolnikov8 years ago15import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
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) {
0a68f8dbArtem Egorov8 years ago30const nestedError = ErrorHelper.getNestedWarning(reason, `${message}. Debugging won't work: Try reloading the JS from inside the app, or Reconnect the VS Code debugger`);
31
32logger.error(nestedError.message);
039239d1Vladimir Kotikov9 years ago33}
34
35/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
36* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
37* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
38* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
39*/
40
41export class MultipleLifetimesAppWorker extends EventEmitter {
42public static WORKER_BOOTSTRAP = `
cc70057dVladimir Kotikov9 years ago43// Initialize some variables before react-native code would access them
d64a6928Vladimir Kotikov9 years ago44var onmessage=null, self=global;
cc70057dVladimir Kotikov9 years ago45// Cache Node's original require as __debug__.require
d64a6928Vladimir Kotikov9 years ago46global.__debug__={require: require};
47// avoid Node's GLOBAL deprecation warning
48Object.defineProperty(global, "GLOBAL", {
49configurable: true,
50writable: true,
51enumerable: true,
52value: global
53});
7daed3fcArtem Egorov8 years ago54
55var vscodeHandlers = {
56'vscode_reloadApp': function () {
57try {
58global.require('NativeModules').DevMenu.reload();
59} catch (err) {
60// ignore
61}
62},
63'vscode_showDevMenu': function () {
64try {
65var DevMenu = global.require('NativeModules').DevMenu.show();
66} catch (err) {
67// ignore
68}
69}
70};
71
72process.on("message", function (message) {
73if (message.data && vscodeHandlers[message.data.method]) {
74vscodeHandlers[message.data.method]();
75} else if(onmessage) {
76onmessage(message);
77}
cc70057dVladimir Kotikov9 years ago78});
7daed3fcArtem Egorov8 years ago79
cc70057dVladimir Kotikov9 years ago80var postMessage = function(message){
81process.send(message);
82};
bb869343Serge Svekolnikov8 years ago83
84if (!self.postMessage) {
85self.postMessage = postMessage;
86}
87
cc70057dVladimir Kotikov9 years ago88var importScripts = (function(){
89var fs=require('fs'), vm=require('vm');
90return function(scriptUrl){
91var scriptCode = fs.readFileSync(scriptUrl, "utf8");
92vm.runInThisContext(scriptCode, {filename: scriptUrl});
93};
94})();`;
95
039239d1Vladimir Kotikov9 years ago96public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago97// and started listening for IPC messages
98postMessage({workerLoaded:true});`;
99
bb869343Serge Svekolnikov8 years ago100public static FETCH_STUB = `(function(self) {
101'use strict';
102
103if (self.fetch) {
104return
105}
106
107self.fetch = fetch;
108
109function fetch(url) {
110return new Promise((resolve, reject) => {
111var data = require("fs").readFileSync(url, 'utf8');
112resolve(
113{
114text: function () {
115return data;
116}
117});
118});
119}
120})(global);`;
121
6eeec3c0Serge Svekolnikov8 years ago122private packagerAddress: string;
e3ae4227digeff10 years ago123private packagerPort: number;
4677921cdigeff10 years ago124private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago125private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago126private packagerRemoteRoot?: string;
127private packagerLocalRoot?: string;
ea8a5f88digeff10 years ago128private socketToApp: WebSocket;
5c8365a6Artem Egorov8 years ago129private singleLifetimeWorker: IDebuggeeWorker | null;
3b6023b2Jimmy Thomson10 years ago130private webSocketConstructor: (url: string) => WebSocket;
131
7cc67271digeff10 years ago132private executionLimiter = new ExecutionsLimiter();
cc70057dVladimir Kotikov9 years ago133private nodeFileSystem = new NodeFileSystem();
134private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago135
6eeec3c0Serge Svekolnikov8 years ago136constructor(
137attachRequestArguments: any,
138sourcesStoragePath: string,
139projectRootPath: string,
140{
141webSocketConstructor = (url: string) => new WebSocket(url),
142} = {}) {
e45838cbVladimir Kotikov9 years ago143super();
6eeec3c0Serge Svekolnikov8 years ago144this.packagerAddress = attachRequestArguments.address || "localhost";
145this.packagerPort = attachRequestArguments.port;
146this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
147this.packagerLocalRoot = attachRequestArguments.localRoot;
4677921cdigeff10 years ago148this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago149this.projectRootPath = projectRootPath;
ea8a5f88digeff10 years ago150console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
3b6023b2Jimmy Thomson10 years ago151
152this.webSocketConstructor = webSocketConstructor;
6eeec3c0Serge Svekolnikov8 years ago153this.scriptImporter = new ScriptImporter(this.packagerAddress, this.packagerPort, sourcesStoragePath, this.packagerRemoteRoot, this.packagerLocalRoot);
4677921cdigeff10 years ago154}
155
cc70057dVladimir Kotikov9 years ago156public start(retryAttempt: boolean = false): Q.Promise<any> {
0a68f8dbArtem Egorov8 years ago157const errPackagerNotRunning = 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.`);
158
6eeec3c0Serge Svekolnikov8 years ago159return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
cc70057dVladimir Kotikov9 years ago160.then(() => {
161// Don't fetch debugger worker on socket disconnect
162return retryAttempt ? Q.resolve<void>(void 0) :
163this.downloadAndPatchDebuggerWorker();
164})
165.then(() => this.createSocketToApp(retryAttempt));
ff7dce65digeff10 years ago166}
167
e45838cbVladimir Kotikov9 years ago168public stop() {
169if (this.socketToApp) {
170this.socketToApp.removeAllListeners();
171this.socketToApp.close();
172}
173
174if (this.singleLifetimeWorker) {
175this.singleLifetimeWorker.stop();
176}
177}
178
cc70057dVladimir Kotikov9 years ago179public downloadAndPatchDebuggerWorker(): Q.Promise<void> {
180let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
a1324704Artem Egorov8 years ago181return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath, this.projectRootPath)
cc70057dVladimir Kotikov9 years ago182.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
183.then((workerContent: string) => {
bb869343Serge Svekolnikov8 years ago184const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
cc70057dVladimir Kotikov9 years ago185// Add our customizations to debugger worker to get it working smoothly
186// in Node env and polyfill WebWorkers API over Node's IPC.
bb869343Serge Svekolnikov8 years ago187const modifiedDebuggeeContent = [
188MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
189isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
190workerContent,
191MultipleLifetimesAppWorker.WORKER_DONE,
192].join("\n");
cc70057dVladimir Kotikov9 years ago193return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
194});
195}
196
ff7dce65digeff10 years ago197private startNewWorkerLifetime(): Q.Promise<void> {
6eeec3c0Serge Svekolnikov8 years ago198this.singleLifetimeWorker = new ForkedAppWorker(this.packagerAddress, this.packagerPort, this.sourcesStoragePath, this.projectRootPath,
199(message) => {
200this.sendMessageToApp(message);
201},
202this.packagerRemoteRoot, this.packagerLocalRoot);
0a68f8dbArtem Egorov8 years ago203logger.verbose("A new app worker lifetime was created.");
e45838cbVladimir Kotikov9 years ago204return this.singleLifetimeWorker.start()
cc70057dVladimir Kotikov9 years ago205.then(startedEvent => {
206this.emit("connected", startedEvent);
207});
4677921cdigeff10 years ago208}
209
cc70057dVladimir Kotikov9 years ago210private createSocketToApp(retryAttempt: boolean = false): Q.Promise<void> {
b9356af0Meena Kunnathur Balakrishnan10 years ago211let deferred = Q.defer<void>();
6e731058Jimmy Thomson10 years ago212this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
b9356af0Meena Kunnathur Balakrishnan10 years ago213this.socketToApp.on("open", () => {
214this.onSocketOpened();
215});
299b0557Patricio Beltran10 years ago216this.socketToApp.on("close",
217() => {
218this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
219/*
220* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
221* it closes the socket because it already has a connection to a debugger.
222* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
223*/
6d5c8798Nikita Matrosov9 years ago224let msgKey = "_closeMessage";
225if (this.socketToApp[msgKey] === "Another debugger is already connected") {
299b0557Patricio Beltran10 years ago226deferred.reject(new RangeError("Another debugger is already connected to packager. Please close it before trying to debug with VSCode."));
227}
0a68f8dbArtem Egorov8 years ago228logger.log("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...");
299b0557Patricio Beltran10 years ago229});
230setTimeout(() => {
231this.start(true /* retryAttempt */);
232}, 100);
233});
b9356af0Meena Kunnathur Balakrishnan10 years ago234this.socketToApp.on("message",
ea8a5f88digeff10 years ago235(message: any) => this.onMessage(message));
b9356af0Meena Kunnathur Balakrishnan10 years ago236this.socketToApp.on("error",
32cab018Meena Kunnathur Balakrishnan10 years ago237(error: Error) => {
cc70057dVladimir Kotikov9 years ago238if (retryAttempt) {
0a68f8dbArtem Egorov8 years ago239printDebuggingError("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.", error);
499fe4ebMeena Kunnathur Balakrishnan10 years ago240}
241
32cab018Meena Kunnathur Balakrishnan10 years ago242deferred.reject(error);
243});
244
499fe4ebMeena Kunnathur Balakrishnan10 years ago245// In an attempt to catch failures in starting the packager on first attempt,
246// wait for 300 ms before resolving the promise
b9356af0Meena Kunnathur Balakrishnan10 years ago247Q.delay(300).done(() => deferred.resolve(void 0));
32cab018Meena Kunnathur Balakrishnan10 years ago248return deferred.promise;
4677921cdigeff10 years ago249}
250
251private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago252return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago253}
254
ea8a5f88digeff10 years ago255private onSocketOpened() {
7cc67271digeff10 years ago256this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
0a68f8dbArtem Egorov8 years ago257logger.log("Established a connection with the Proxy (Packager) to the React Native application"));
4677921cdigeff10 years ago258}
259
9174feb7Vladimir Kotikov9 years ago260private killWorker() {
261if (!this.singleLifetimeWorker) return;
262this.singleLifetimeWorker.stop();
263this.singleLifetimeWorker = null;
264}
e7b314e8Vladimir Kotikov9 years ago265
9174feb7Vladimir Kotikov9 years ago266private onMessage(message: string) {
5d4d4de0digeff10 years ago267try {
0a68f8dbArtem Egorov8 years ago268logger.verbose("From RN APP: " + message);
ea8a5f88digeff10 years ago269let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago270if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago271// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
272// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago273this.killWorker();
5d4d4de0digeff10 years ago274// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
275this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago276} else if (object.method === "$disconnected") {
277// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago278this.killWorker();
5d4d4de0digeff10 years ago279} else if (object.method) {
280// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago281if (this.singleLifetimeWorker) {
282this.singleLifetimeWorker.postMessage(object);
283}
5d4d4de0digeff10 years ago284} else {
ea8a5f88digeff10 years ago285// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
0a68f8dbArtem Egorov8 years ago286logger.verbose("The react-native app sent a message without specifying a method: " + message);
5d4d4de0digeff10 years ago287}
288} catch (exception) {
ce591c62digeff10 years ago289printDebuggingError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
4677921cdigeff10 years ago290}
291}
292
293private gotPrepareJSRuntime(message: any): void {
294// Create the sandbox, and replay that we finished processing the message
ff7dce65digeff10 years ago295this.startNewWorkerLifetime().done(() => {
296this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
297}, error => printDebuggingError(`Failed to prepare the JavaScript runtime environment. Message:\n${message}`, error));
4677921cdigeff10 years ago298}
299
ff7dce65digeff10 years ago300private sendMessageToApp(message: any): void {
5c8365a6Artem Egorov8 years ago301let stringified: string = "";
354c28a1digeff10 years ago302try {
303stringified = JSON.stringify(message);
0a68f8dbArtem Egorov8 years ago304logger.verbose("To RN APP: " + stringified);
354c28a1digeff10 years ago305this.socketToApp.send(stringified);
306} catch (exception) {
307let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
ce591c62digeff10 years ago308printDebuggingError(`Failed to send message to the React Native app. Message:\n${messageToShow}`, exception);
354c28a1digeff10 years ago309}
4677921cdigeff10 years ago310}
311}