microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.6.16

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

316lines · 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});
0864d702Ruslan Bikkinin7 years ago54// Prevent leaking process.versions from debugger process to
55// worker because pure React Native doesn't do that and some packages as js-md5 rely on this behavior
56Object.defineProperty(process, "versions", {
57value: undefined
58});
7daed3fcArtem Egorov8 years ago59
60var vscodeHandlers = {
61'vscode_reloadApp': function () {
62try {
63global.require('NativeModules').DevMenu.reload();
64} catch (err) {
65// ignore
66}
67},
68'vscode_showDevMenu': function () {
69try {
70var DevMenu = global.require('NativeModules').DevMenu.show();
71} catch (err) {
72// ignore
73}
74}
75};
76
77process.on("message", function (message) {
78if (message.data && vscodeHandlers[message.data.method]) {
79vscodeHandlers[message.data.method]();
80} else if(onmessage) {
81onmessage(message);
82}
cc70057dVladimir Kotikov9 years ago83});
7daed3fcArtem Egorov8 years ago84
cc70057dVladimir Kotikov9 years ago85var postMessage = function(message){
86process.send(message);
87};
bb869343Serge Svekolnikov8 years ago88
89if (!self.postMessage) {
90self.postMessage = postMessage;
91}
92
cc70057dVladimir Kotikov9 years ago93var importScripts = (function(){
94var fs=require('fs'), vm=require('vm');
95return function(scriptUrl){
96var scriptCode = fs.readFileSync(scriptUrl, "utf8");
97vm.runInThisContext(scriptCode, {filename: scriptUrl});
98};
99})();`;
100
039239d1Vladimir Kotikov9 years ago101public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago102// and started listening for IPC messages
103postMessage({workerLoaded:true});`;
104
bb869343Serge Svekolnikov8 years ago105public static FETCH_STUB = `(function(self) {
106'use strict';
107
108if (self.fetch) {
109return
110}
111
112self.fetch = fetch;
113
114function fetch(url) {
115return new Promise((resolve, reject) => {
116var data = require("fs").readFileSync(url, 'utf8');
117resolve(
118{
119text: function () {
120return data;
121}
122});
123});
124}
125})(global);`;
126
6eeec3c0Serge Svekolnikov8 years ago127private packagerAddress: string;
e3ae4227digeff10 years ago128private packagerPort: number;
4677921cdigeff10 years ago129private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago130private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago131private packagerRemoteRoot?: string;
132private packagerLocalRoot?: string;
ea8a5f88digeff10 years ago133private socketToApp: WebSocket;
5c8365a6Artem Egorov8 years ago134private singleLifetimeWorker: IDebuggeeWorker | null;
3b6023b2Jimmy Thomson10 years ago135private webSocketConstructor: (url: string) => WebSocket;
136
7cc67271digeff10 years ago137private executionLimiter = new ExecutionsLimiter();
cc70057dVladimir Kotikov9 years ago138private nodeFileSystem = new NodeFileSystem();
139private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago140
6eeec3c0Serge Svekolnikov8 years ago141constructor(
142attachRequestArguments: any,
143sourcesStoragePath: string,
144projectRootPath: string,
145{
146webSocketConstructor = (url: string) => new WebSocket(url),
147} = {}) {
e45838cbVladimir Kotikov9 years ago148super();
6eeec3c0Serge Svekolnikov8 years ago149this.packagerAddress = attachRequestArguments.address || "localhost";
150this.packagerPort = attachRequestArguments.port;
151this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
152this.packagerLocalRoot = attachRequestArguments.localRoot;
4677921cdigeff10 years ago153this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago154this.projectRootPath = projectRootPath;
ea8a5f88digeff10 years ago155console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
3b6023b2Jimmy Thomson10 years ago156
157this.webSocketConstructor = webSocketConstructor;
6eeec3c0Serge Svekolnikov8 years ago158this.scriptImporter = new ScriptImporter(this.packagerAddress, this.packagerPort, sourcesStoragePath, this.packagerRemoteRoot, this.packagerLocalRoot);
4677921cdigeff10 years ago159}
160
cc70057dVladimir Kotikov9 years ago161public start(retryAttempt: boolean = false): Q.Promise<any> {
0a68f8dbArtem Egorov8 years ago162const 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.`);
163
6eeec3c0Serge Svekolnikov8 years ago164return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
cc70057dVladimir Kotikov9 years ago165.then(() => {
166// Don't fetch debugger worker on socket disconnect
167return retryAttempt ? Q.resolve<void>(void 0) :
168this.downloadAndPatchDebuggerWorker();
169})
170.then(() => this.createSocketToApp(retryAttempt));
ff7dce65digeff10 years ago171}
172
e45838cbVladimir Kotikov9 years ago173public stop() {
174if (this.socketToApp) {
175this.socketToApp.removeAllListeners();
176this.socketToApp.close();
177}
178
179if (this.singleLifetimeWorker) {
180this.singleLifetimeWorker.stop();
181}
182}
183
cc70057dVladimir Kotikov9 years ago184public downloadAndPatchDebuggerWorker(): Q.Promise<void> {
185let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
a1324704Artem Egorov8 years ago186return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath, this.projectRootPath)
cc70057dVladimir Kotikov9 years ago187.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
188.then((workerContent: string) => {
bb869343Serge Svekolnikov8 years ago189const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
cc70057dVladimir Kotikov9 years ago190// Add our customizations to debugger worker to get it working smoothly
191// in Node env and polyfill WebWorkers API over Node's IPC.
bb869343Serge Svekolnikov8 years ago192const modifiedDebuggeeContent = [
193MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
194isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
195workerContent,
196MultipleLifetimesAppWorker.WORKER_DONE,
197].join("\n");
cc70057dVladimir Kotikov9 years ago198return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
199});
200}
201
ff7dce65digeff10 years ago202private startNewWorkerLifetime(): Q.Promise<void> {
6eeec3c0Serge Svekolnikov8 years ago203this.singleLifetimeWorker = new ForkedAppWorker(this.packagerAddress, this.packagerPort, this.sourcesStoragePath, this.projectRootPath,
204(message) => {
205this.sendMessageToApp(message);
206},
207this.packagerRemoteRoot, this.packagerLocalRoot);
0a68f8dbArtem Egorov8 years ago208logger.verbose("A new app worker lifetime was created.");
e45838cbVladimir Kotikov9 years ago209return this.singleLifetimeWorker.start()
cc70057dVladimir Kotikov9 years ago210.then(startedEvent => {
211this.emit("connected", startedEvent);
212});
4677921cdigeff10 years ago213}
214
cc70057dVladimir Kotikov9 years ago215private createSocketToApp(retryAttempt: boolean = false): Q.Promise<void> {
b9356af0Meena Kunnathur Balakrishnan10 years ago216let deferred = Q.defer<void>();
6e731058Jimmy Thomson10 years ago217this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
b9356af0Meena Kunnathur Balakrishnan10 years ago218this.socketToApp.on("open", () => {
219this.onSocketOpened();
220});
299b0557Patricio Beltran10 years ago221this.socketToApp.on("close",
222() => {
223this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
224/*
225* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
226* it closes the socket because it already has a connection to a debugger.
227* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
228*/
6d5c8798Nikita Matrosov9 years ago229let msgKey = "_closeMessage";
230if (this.socketToApp[msgKey] === "Another debugger is already connected") {
299b0557Patricio Beltran10 years ago231deferred.reject(new RangeError("Another debugger is already connected to packager. Please close it before trying to debug with VSCode."));
232}
0a68f8dbArtem Egorov8 years ago233logger.log("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...");
299b0557Patricio Beltran10 years ago234});
235setTimeout(() => {
236this.start(true /* retryAttempt */);
237}, 100);
238});
b9356af0Meena Kunnathur Balakrishnan10 years ago239this.socketToApp.on("message",
ea8a5f88digeff10 years ago240(message: any) => this.onMessage(message));
b9356af0Meena Kunnathur Balakrishnan10 years ago241this.socketToApp.on("error",
32cab018Meena Kunnathur Balakrishnan10 years ago242(error: Error) => {
cc70057dVladimir Kotikov9 years ago243if (retryAttempt) {
0a68f8dbArtem Egorov8 years ago244printDebuggingError("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 ago245}
246
32cab018Meena Kunnathur Balakrishnan10 years ago247deferred.reject(error);
248});
249
499fe4ebMeena Kunnathur Balakrishnan10 years ago250// In an attempt to catch failures in starting the packager on first attempt,
251// wait for 300 ms before resolving the promise
b9356af0Meena Kunnathur Balakrishnan10 years ago252Q.delay(300).done(() => deferred.resolve(void 0));
32cab018Meena Kunnathur Balakrishnan10 years ago253return deferred.promise;
4677921cdigeff10 years ago254}
255
256private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago257return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago258}
259
ea8a5f88digeff10 years ago260private onSocketOpened() {
7cc67271digeff10 years ago261this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
0a68f8dbArtem Egorov8 years ago262logger.log("Established a connection with the Proxy (Packager) to the React Native application"));
4677921cdigeff10 years ago263}
264
9174feb7Vladimir Kotikov9 years ago265private killWorker() {
266if (!this.singleLifetimeWorker) return;
267this.singleLifetimeWorker.stop();
268this.singleLifetimeWorker = null;
269}
e7b314e8Vladimir Kotikov9 years ago270
9174feb7Vladimir Kotikov9 years ago271private onMessage(message: string) {
5d4d4de0digeff10 years ago272try {
0a68f8dbArtem Egorov8 years ago273logger.verbose("From RN APP: " + message);
ea8a5f88digeff10 years ago274let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago275if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago276// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
277// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago278this.killWorker();
5d4d4de0digeff10 years ago279// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
280this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago281} else if (object.method === "$disconnected") {
282// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago283this.killWorker();
5d4d4de0digeff10 years ago284} else if (object.method) {
285// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago286if (this.singleLifetimeWorker) {
287this.singleLifetimeWorker.postMessage(object);
288}
5d4d4de0digeff10 years ago289} else {
ea8a5f88digeff10 years ago290// 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 ago291logger.verbose("The react-native app sent a message without specifying a method: " + message);
5d4d4de0digeff10 years ago292}
293} catch (exception) {
ce591c62digeff10 years ago294printDebuggingError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
4677921cdigeff10 years ago295}
296}
297
298private gotPrepareJSRuntime(message: any): void {
299// Create the sandbox, and replay that we finished processing the message
ff7dce65digeff10 years ago300this.startNewWorkerLifetime().done(() => {
301this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
302}, error => printDebuggingError(`Failed to prepare the JavaScript runtime environment. Message:\n${message}`, error));
4677921cdigeff10 years ago303}
304
ff7dce65digeff10 years ago305private sendMessageToApp(message: any): void {
5c8365a6Artem Egorov8 years ago306let stringified: string = "";
354c28a1digeff10 years ago307try {
308stringified = JSON.stringify(message);
0a68f8dbArtem Egorov8 years ago309logger.verbose("To RN APP: " + stringified);
354c28a1digeff10 years ago310this.socketToApp.send(stringified);
311} catch (exception) {
312let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
ce591c62digeff10 years ago313printDebuggingError(`Failed to send message to the React Native app. Message:\n${messageToShow}`, exception);
354c28a1digeff10 years ago314}
4677921cdigeff10 years ago315}
316}