microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.8.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

319lines · 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";
d124bf0eYuri Skorokhodov7 years ago16import * as nls from "vscode-nls";
17import { InternalErrorCode } from "../common/error/internalErrorCode";
18const localize = nls.loadMessageBundle();
4677921cdigeff10 years ago19
e45838cbVladimir Kotikov9 years ago20export interface RNAppMessage {
ea8a5f88digeff10 years ago21method: string;
e45838cbVladimir Kotikov9 years ago22url?: string;
ea8a5f88digeff10 years ago23// These objects have also other properties but that we don't currently use
24}
5d4d4de0digeff10 years ago25
e45838cbVladimir Kotikov9 years ago26export interface IDebuggeeWorker {
27start(): Q.Promise<any>;
28stop(): void;
29postMessage(message: RNAppMessage): void;
30}
31
1758f9a6Yuri Skorokhodov7 years ago32function printDebuggingError(error: Error, reason: any) {
33const nestedError = ErrorHelper.getNestedError(error, InternalErrorCode.DebuggingWontWorkReloadJSAndReconnect, reason);
0a68f8dbArtem Egorov8 years ago34
35logger.error(nestedError.message);
039239d1Vladimir Kotikov9 years ago36}
37
38/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
39* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
40* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
41* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
42*/
43
44export class MultipleLifetimesAppWorker extends EventEmitter {
45public static WORKER_BOOTSTRAP = `
cc70057dVladimir Kotikov9 years ago46// Initialize some variables before react-native code would access them
d64a6928Vladimir Kotikov9 years ago47var onmessage=null, self=global;
cc70057dVladimir Kotikov9 years ago48// Cache Node's original require as __debug__.require
d64a6928Vladimir Kotikov9 years ago49global.__debug__={require: require};
50// avoid Node's GLOBAL deprecation warning
51Object.defineProperty(global, "GLOBAL", {
52configurable: true,
53writable: true,
54enumerable: true,
55value: global
56});
0864d702Ruslan Bikkinin7 years ago57// Prevent leaking process.versions from debugger process to
58// worker because pure React Native doesn't do that and some packages as js-md5 rely on this behavior
59Object.defineProperty(process, "versions", {
60value: undefined
61});
7daed3fcArtem Egorov8 years ago62
63var vscodeHandlers = {
64'vscode_reloadApp': function () {
65try {
66global.require('NativeModules').DevMenu.reload();
67} catch (err) {
68// ignore
69}
70},
71'vscode_showDevMenu': function () {
72try {
73var DevMenu = global.require('NativeModules').DevMenu.show();
74} catch (err) {
75// ignore
76}
77}
78};
79
80process.on("message", function (message) {
81if (message.data && vscodeHandlers[message.data.method]) {
82vscodeHandlers[message.data.method]();
83} else if(onmessage) {
84onmessage(message);
85}
cc70057dVladimir Kotikov9 years ago86});
7daed3fcArtem Egorov8 years ago87
cc70057dVladimir Kotikov9 years ago88var postMessage = function(message){
89process.send(message);
90};
bb869343Serge Svekolnikov8 years ago91
92if (!self.postMessage) {
93self.postMessage = postMessage;
94}
95
cc70057dVladimir Kotikov9 years ago96var importScripts = (function(){
97var fs=require('fs'), vm=require('vm');
98return function(scriptUrl){
99var scriptCode = fs.readFileSync(scriptUrl, "utf8");
100vm.runInThisContext(scriptCode, {filename: scriptUrl});
101};
102})();`;
103
039239d1Vladimir Kotikov9 years ago104public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago105// and started listening for IPC messages
106postMessage({workerLoaded:true});`;
107
bb869343Serge Svekolnikov8 years ago108public static FETCH_STUB = `(function(self) {
109'use strict';
110
111if (self.fetch) {
112return
113}
114
115self.fetch = fetch;
116
117function fetch(url) {
118return new Promise((resolve, reject) => {
119var data = require("fs").readFileSync(url, 'utf8');
120resolve(
121{
122text: function () {
123return data;
124}
125});
126});
127}
128})(global);`;
129
6eeec3c0Serge Svekolnikov8 years ago130private packagerAddress: string;
e3ae4227digeff10 years ago131private packagerPort: number;
4677921cdigeff10 years ago132private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago133private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago134private packagerRemoteRoot?: string;
135private packagerLocalRoot?: string;
ea8a5f88digeff10 years ago136private socketToApp: WebSocket;
5c8365a6Artem Egorov8 years ago137private singleLifetimeWorker: IDebuggeeWorker | null;
3b6023b2Jimmy Thomson10 years ago138private webSocketConstructor: (url: string) => WebSocket;
139
7cc67271digeff10 years ago140private executionLimiter = new ExecutionsLimiter();
cc70057dVladimir Kotikov9 years ago141private nodeFileSystem = new NodeFileSystem();
142private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago143
6eeec3c0Serge Svekolnikov8 years ago144constructor(
145attachRequestArguments: any,
146sourcesStoragePath: string,
147projectRootPath: string,
148{
149webSocketConstructor = (url: string) => new WebSocket(url),
150} = {}) {
e45838cbVladimir Kotikov9 years ago151super();
6eeec3c0Serge Svekolnikov8 years ago152this.packagerAddress = attachRequestArguments.address || "localhost";
153this.packagerPort = attachRequestArguments.port;
154this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
155this.packagerLocalRoot = attachRequestArguments.localRoot;
4677921cdigeff10 years ago156this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago157this.projectRootPath = projectRootPath;
1758f9a6Yuri Skorokhodov7 years ago158if (!this.sourcesStoragePath)
159throw ErrorHelper.getInternalError(InternalErrorCode.SourcesStoragePathIsNullOrEmpty);
3b6023b2Jimmy Thomson10 years ago160this.webSocketConstructor = webSocketConstructor;
6eeec3c0Serge Svekolnikov8 years ago161this.scriptImporter = new ScriptImporter(this.packagerAddress, this.packagerPort, sourcesStoragePath, this.packagerRemoteRoot, this.packagerLocalRoot);
4677921cdigeff10 years ago162}
163
cc70057dVladimir Kotikov9 years ago164public start(retryAttempt: boolean = false): Q.Promise<any> {
d124bf0eYuri Skorokhodov7 years ago165const errPackagerNotRunning = ErrorHelper.getInternalError(InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort, this.packagerPort);
0a68f8dbArtem Egorov8 years ago166
6eeec3c0Serge Svekolnikov8 years ago167return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
cc70057dVladimir Kotikov9 years ago168.then(() => {
169// Don't fetch debugger worker on socket disconnect
170return retryAttempt ? Q.resolve<void>(void 0) :
171this.downloadAndPatchDebuggerWorker();
172})
173.then(() => this.createSocketToApp(retryAttempt));
ff7dce65digeff10 years ago174}
175
e45838cbVladimir Kotikov9 years ago176public stop() {
177if (this.socketToApp) {
178this.socketToApp.removeAllListeners();
179this.socketToApp.close();
180}
181
182if (this.singleLifetimeWorker) {
183this.singleLifetimeWorker.stop();
184}
185}
186
cc70057dVladimir Kotikov9 years ago187public downloadAndPatchDebuggerWorker(): Q.Promise<void> {
188let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
a1324704Artem Egorov8 years ago189return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath, this.projectRootPath)
cc70057dVladimir Kotikov9 years ago190.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
191.then((workerContent: string) => {
bb869343Serge Svekolnikov8 years ago192const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
cc70057dVladimir Kotikov9 years ago193// Add our customizations to debugger worker to get it working smoothly
194// in Node env and polyfill WebWorkers API over Node's IPC.
bb869343Serge Svekolnikov8 years ago195const modifiedDebuggeeContent = [
196MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
197isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
198workerContent,
199MultipleLifetimesAppWorker.WORKER_DONE,
200].join("\n");
cc70057dVladimir Kotikov9 years ago201return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
202});
203}
204
ff7dce65digeff10 years ago205private startNewWorkerLifetime(): Q.Promise<void> {
6eeec3c0Serge Svekolnikov8 years ago206this.singleLifetimeWorker = new ForkedAppWorker(this.packagerAddress, this.packagerPort, this.sourcesStoragePath, this.projectRootPath,
207(message) => {
208this.sendMessageToApp(message);
209},
210this.packagerRemoteRoot, this.packagerLocalRoot);
0a68f8dbArtem Egorov8 years ago211logger.verbose("A new app worker lifetime was created.");
e45838cbVladimir Kotikov9 years ago212return this.singleLifetimeWorker.start()
cc70057dVladimir Kotikov9 years ago213.then(startedEvent => {
214this.emit("connected", startedEvent);
215});
4677921cdigeff10 years ago216}
217
cc70057dVladimir Kotikov9 years ago218private createSocketToApp(retryAttempt: boolean = false): Q.Promise<void> {
b9356af0Meena Kunnathur Balakrishnan10 years ago219let deferred = Q.defer<void>();
6e731058Jimmy Thomson10 years ago220this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
b9356af0Meena Kunnathur Balakrishnan10 years ago221this.socketToApp.on("open", () => {
222this.onSocketOpened();
223});
299b0557Patricio Beltran10 years ago224this.socketToApp.on("close",
225() => {
226this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
227/*
228* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
229* it closes the socket because it already has a connection to a debugger.
230* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
231*/
6d5c8798Nikita Matrosov9 years ago232let msgKey = "_closeMessage";
233if (this.socketToApp[msgKey] === "Another debugger is already connected") {
d124bf0eYuri Skorokhodov7 years ago234deferred.reject(ErrorHelper.getInternalError(InternalErrorCode.AnotherDebuggerConnectedToPackager));
299b0557Patricio Beltran10 years ago235}
d124bf0eYuri Skorokhodov7 years ago236logger.log(localize("DisconnectedFromThePackagerToReactNative", "Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon..."));
299b0557Patricio Beltran10 years ago237});
238setTimeout(() => {
239this.start(true /* retryAttempt */);
240}, 100);
241});
b9356af0Meena Kunnathur Balakrishnan10 years ago242this.socketToApp.on("message",
ea8a5f88digeff10 years ago243(message: any) => this.onMessage(message));
b9356af0Meena Kunnathur Balakrishnan10 years ago244this.socketToApp.on("error",
32cab018Meena Kunnathur Balakrishnan10 years ago245(error: Error) => {
cc70057dVladimir Kotikov9 years ago246if (retryAttempt) {
1758f9a6Yuri Skorokhodov7 years ago247printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.ReconnectionToPackagerFailedCheckForErrorsOrRestartReactNative), error);
499fe4ebMeena Kunnathur Balakrishnan10 years ago248}
249
32cab018Meena Kunnathur Balakrishnan10 years ago250deferred.reject(error);
251});
252
499fe4ebMeena Kunnathur Balakrishnan10 years ago253// In an attempt to catch failures in starting the packager on first attempt,
254// wait for 300 ms before resolving the promise
b9356af0Meena Kunnathur Balakrishnan10 years ago255Q.delay(300).done(() => deferred.resolve(void 0));
32cab018Meena Kunnathur Balakrishnan10 years ago256return deferred.promise;
4677921cdigeff10 years ago257}
258
259private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago260return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago261}
262
ea8a5f88digeff10 years ago263private onSocketOpened() {
7cc67271digeff10 years ago264this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
d124bf0eYuri Skorokhodov7 years ago265logger.log(localize("EstablishedConnectionWithPackagerToReactNativeApp", "Established a connection with the Proxy (Packager) to the React Native application")));
4677921cdigeff10 years ago266}
267
9174feb7Vladimir Kotikov9 years ago268private killWorker() {
269if (!this.singleLifetimeWorker) return;
270this.singleLifetimeWorker.stop();
271this.singleLifetimeWorker = null;
272}
e7b314e8Vladimir Kotikov9 years ago273
9174feb7Vladimir Kotikov9 years ago274private onMessage(message: string) {
5d4d4de0digeff10 years ago275try {
0a68f8dbArtem Egorov8 years ago276logger.verbose("From RN APP: " + message);
ea8a5f88digeff10 years ago277let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago278if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago279// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
280// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago281this.killWorker();
5d4d4de0digeff10 years ago282// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
283this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago284} else if (object.method === "$disconnected") {
285// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago286this.killWorker();
5d4d4de0digeff10 years ago287} else if (object.method) {
288// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago289if (this.singleLifetimeWorker) {
290this.singleLifetimeWorker.postMessage(object);
291}
5d4d4de0digeff10 years ago292} else {
ea8a5f88digeff10 years ago293// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
d124bf0eYuri Skorokhodov7 years ago294logger.verbose(`The react-native app sent a message without specifying a method: ${message}`);
5d4d4de0digeff10 years ago295}
296} catch (exception) {
1758f9a6Yuri Skorokhodov7 years ago297printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToProcessMessageFromReactNativeApp, message), exception);
4677921cdigeff10 years ago298}
299}
300
301private gotPrepareJSRuntime(message: any): void {
302// Create the sandbox, and replay that we finished processing the message
ff7dce65digeff10 years ago303this.startNewWorkerLifetime().done(() => {
304this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
1758f9a6Yuri Skorokhodov7 years ago305}, error => printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToPrepareJSRuntimeEnvironment, message), error));
4677921cdigeff10 years ago306}
307
ff7dce65digeff10 years ago308private sendMessageToApp(message: any): void {
5c8365a6Artem Egorov8 years ago309let stringified: string = "";
354c28a1digeff10 years ago310try {
311stringified = JSON.stringify(message);
d124bf0eYuri Skorokhodov7 years ago312logger.verbose(`To RN APP: ${stringified}`);
354c28a1digeff10 years ago313this.socketToApp.send(stringified);
314} catch (exception) {
315let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
1758f9a6Yuri Skorokhodov7 years ago316printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToSendMessageToTheReactNativeApp, messageToShow), exception);
354c28a1digeff10 years ago317}
4677921cdigeff10 years ago318}
319}