microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.10.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

372lines · 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
2ecfbd20Yuri Skorokhodov7 years ago63function getNativeModules() {
64var NativeModules;
65try {
66// This approach is for old RN versions
67NativeModules = global.require('NativeModules');
68} catch (err) {
69// ignore error and try another way for more recent RN versions
70try {
71var nativeModuleId;
72var modules = global.__r.getModules();
73var ids = Object.keys(modules);
74for (var i = 0; i < ids.length; i++) {
75if (modules[ids[i]].verboseName) {
76var packagePath = new String(modules[ids[i]].verboseName);
77if (packagePath.indexOf("react-native/Libraries/BatchedBridge/NativeModules.js") > 0) {
78nativeModuleId = parseInt(ids[i], 10);
79break;
80}
81}
82}
83if (nativeModuleId) {
84NativeModules = global.__r(nativeModuleId);
85}
86}
87catch (err) {
88// suppress errors
89}
90}
91return NativeModules;
92}
93
94// Originally, this was made for iOS only
7daed3fcArtem Egorov8 years ago95var vscodeHandlers = {
96'vscode_reloadApp': function () {
2ecfbd20Yuri Skorokhodov7 years ago97var NativeModules = getNativeModules();
98if (NativeModules) {
99NativeModules.DevMenu.reload();
7daed3fcArtem Egorov8 years ago100}
101},
102'vscode_showDevMenu': function () {
2ecfbd20Yuri Skorokhodov7 years ago103var NativeModules = getNativeModules();
104if (NativeModules) {
105NativeModules.DevMenu.show();
7daed3fcArtem Egorov8 years ago106}
107}
108};
109
110process.on("message", function (message) {
111if (message.data && vscodeHandlers[message.data.method]) {
112vscodeHandlers[message.data.method]();
113} else if(onmessage) {
114onmessage(message);
115}
cc70057dVladimir Kotikov9 years ago116});
7daed3fcArtem Egorov8 years ago117
cc70057dVladimir Kotikov9 years ago118var postMessage = function(message){
119process.send(message);
120};
bb869343Serge Svekolnikov8 years ago121
122if (!self.postMessage) {
123self.postMessage = postMessage;
124}
125
cc70057dVladimir Kotikov9 years ago126var importScripts = (function(){
127var fs=require('fs'), vm=require('vm');
128return function(scriptUrl){
129var scriptCode = fs.readFileSync(scriptUrl, "utf8");
130vm.runInThisContext(scriptCode, {filename: scriptUrl});
131};
132})();`;
133
cf6dd6b9Yuri Skorokhodov7 years ago134public static CONSOLE_TRACE_PATCH = `// Worker is ran as nodejs process, so console.trace() writes to stderr and it leads to error in native app
135// To avoid this console.trace() is overridden to print stacktrace via console.log()
136// Please, see Node JS implementation: https://github.com/nodejs/node/blob/master/lib/internal/console/constructor.js
137console.trace = (function() {
138return function() {
139try {
140var err = {
141name: 'Trace',
142message: require('util').format.apply(null, arguments)
143};
144// Node uses 10, but usually it's not enough for RN app trace
145Error.stackTraceLimit = 30;
146Error.captureStackTrace(err, console.trace);
147console.log(err.stack);
148} catch (e) {
149console.error(e);
150}
151};
152})();`;
153
039239d1Vladimir Kotikov9 years ago154public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago155// and started listening for IPC messages
156postMessage({workerLoaded:true});`;
157
bb869343Serge Svekolnikov8 years ago158public static FETCH_STUB = `(function(self) {
159'use strict';
160
161if (self.fetch) {
162return
163}
164
165self.fetch = fetch;
166
167function fetch(url) {
168return new Promise((resolve, reject) => {
169var data = require("fs").readFileSync(url, 'utf8');
170resolve(
171{
172text: function () {
173return data;
174}
175});
176});
177}
178})(global);`;
179
6eeec3c0Serge Svekolnikov8 years ago180private packagerAddress: string;
e3ae4227digeff10 years ago181private packagerPort: number;
4677921cdigeff10 years ago182private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago183private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago184private packagerRemoteRoot?: string;
185private packagerLocalRoot?: string;
cf911877Yuri Skorokhodov7 years ago186private debuggerWorkerUrlPath?: string;
ea8a5f88digeff10 years ago187private socketToApp: WebSocket;
5c8365a6Artem Egorov8 years ago188private singleLifetimeWorker: IDebuggeeWorker | null;
3b6023b2Jimmy Thomson10 years ago189private webSocketConstructor: (url: string) => WebSocket;
190
7cc67271digeff10 years ago191private executionLimiter = new ExecutionsLimiter();
cc70057dVladimir Kotikov9 years ago192private nodeFileSystem = new NodeFileSystem();
193private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago194
6eeec3c0Serge Svekolnikov8 years ago195constructor(
196attachRequestArguments: any,
197sourcesStoragePath: string,
198projectRootPath: string,
199{
200webSocketConstructor = (url: string) => new WebSocket(url),
201} = {}) {
e45838cbVladimir Kotikov9 years ago202super();
6eeec3c0Serge Svekolnikov8 years ago203this.packagerAddress = attachRequestArguments.address || "localhost";
204this.packagerPort = attachRequestArguments.port;
205this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
206this.packagerLocalRoot = attachRequestArguments.localRoot;
cf911877Yuri Skorokhodov7 years ago207this.debuggerWorkerUrlPath = attachRequestArguments.debuggerWorkerUrlPath;
4677921cdigeff10 years ago208this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago209this.projectRootPath = projectRootPath;
1758f9a6Yuri Skorokhodov7 years ago210if (!this.sourcesStoragePath)
211throw ErrorHelper.getInternalError(InternalErrorCode.SourcesStoragePathIsNullOrEmpty);
3b6023b2Jimmy Thomson10 years ago212this.webSocketConstructor = webSocketConstructor;
6eeec3c0Serge Svekolnikov8 years ago213this.scriptImporter = new ScriptImporter(this.packagerAddress, this.packagerPort, sourcesStoragePath, this.packagerRemoteRoot, this.packagerLocalRoot);
4677921cdigeff10 years ago214}
215
cc70057dVladimir Kotikov9 years ago216public start(retryAttempt: boolean = false): Q.Promise<any> {
d124bf0eYuri Skorokhodov7 years ago217const errPackagerNotRunning = ErrorHelper.getInternalError(InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort, this.packagerPort);
0a68f8dbArtem Egorov8 years ago218
6eeec3c0Serge Svekolnikov8 years ago219return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
cc70057dVladimir Kotikov9 years ago220.then(() => {
221// Don't fetch debugger worker on socket disconnect
222return retryAttempt ? Q.resolve<void>(void 0) :
223this.downloadAndPatchDebuggerWorker();
224})
225.then(() => this.createSocketToApp(retryAttempt));
ff7dce65digeff10 years ago226}
227
e45838cbVladimir Kotikov9 years ago228public stop() {
229if (this.socketToApp) {
230this.socketToApp.removeAllListeners();
231this.socketToApp.close();
232}
233
234if (this.singleLifetimeWorker) {
235this.singleLifetimeWorker.stop();
236}
237}
238
cc70057dVladimir Kotikov9 years ago239public downloadAndPatchDebuggerWorker(): Q.Promise<void> {
240let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
cf911877Yuri Skorokhodov7 years ago241return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath, this.projectRootPath, this.debuggerWorkerUrlPath)
cc70057dVladimir Kotikov9 years ago242.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
243.then((workerContent: string) => {
bb869343Serge Svekolnikov8 years ago244const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
cc70057dVladimir Kotikov9 years ago245// Add our customizations to debugger worker to get it working smoothly
246// in Node env and polyfill WebWorkers API over Node's IPC.
bb869343Serge Svekolnikov8 years ago247const modifiedDebuggeeContent = [
248MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
cf6dd6b9Yuri Skorokhodov7 years ago249MultipleLifetimesAppWorker.CONSOLE_TRACE_PATCH,
bb869343Serge Svekolnikov8 years ago250isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
251workerContent,
252MultipleLifetimesAppWorker.WORKER_DONE,
253].join("\n");
cc70057dVladimir Kotikov9 years ago254return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
255});
256}
257
ff7dce65digeff10 years ago258private startNewWorkerLifetime(): Q.Promise<void> {
6eeec3c0Serge Svekolnikov8 years ago259this.singleLifetimeWorker = new ForkedAppWorker(this.packagerAddress, this.packagerPort, this.sourcesStoragePath, this.projectRootPath,
260(message) => {
261this.sendMessageToApp(message);
262},
263this.packagerRemoteRoot, this.packagerLocalRoot);
0a68f8dbArtem Egorov8 years ago264logger.verbose("A new app worker lifetime was created.");
e45838cbVladimir Kotikov9 years ago265return this.singleLifetimeWorker.start()
cc70057dVladimir Kotikov9 years ago266.then(startedEvent => {
267this.emit("connected", startedEvent);
268});
4677921cdigeff10 years ago269}
270
cc70057dVladimir Kotikov9 years ago271private createSocketToApp(retryAttempt: boolean = false): Q.Promise<void> {
b9356af0Meena Kunnathur Balakrishnan10 years ago272let deferred = Q.defer<void>();
6e731058Jimmy Thomson10 years ago273this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
b9356af0Meena Kunnathur Balakrishnan10 years ago274this.socketToApp.on("open", () => {
275this.onSocketOpened();
276});
299b0557Patricio Beltran10 years ago277this.socketToApp.on("close",
278() => {
279this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
280/*
281* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
282* it closes the socket because it already has a connection to a debugger.
283* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
284*/
6d5c8798Nikita Matrosov9 years ago285let msgKey = "_closeMessage";
286if (this.socketToApp[msgKey] === "Another debugger is already connected") {
d124bf0eYuri Skorokhodov7 years ago287deferred.reject(ErrorHelper.getInternalError(InternalErrorCode.AnotherDebuggerConnectedToPackager));
299b0557Patricio Beltran10 years ago288}
d124bf0eYuri Skorokhodov7 years ago289logger.log(localize("DisconnectedFromThePackagerToReactNative", "Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon..."));
299b0557Patricio Beltran10 years ago290});
291setTimeout(() => {
292this.start(true /* retryAttempt */);
293}, 100);
294});
b9356af0Meena Kunnathur Balakrishnan10 years ago295this.socketToApp.on("message",
ea8a5f88digeff10 years ago296(message: any) => this.onMessage(message));
b9356af0Meena Kunnathur Balakrishnan10 years ago297this.socketToApp.on("error",
32cab018Meena Kunnathur Balakrishnan10 years ago298(error: Error) => {
cc70057dVladimir Kotikov9 years ago299if (retryAttempt) {
1758f9a6Yuri Skorokhodov7 years ago300printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.ReconnectionToPackagerFailedCheckForErrorsOrRestartReactNative), error);
499fe4ebMeena Kunnathur Balakrishnan10 years ago301}
302
32cab018Meena Kunnathur Balakrishnan10 years ago303deferred.reject(error);
304});
305
499fe4ebMeena Kunnathur Balakrishnan10 years ago306// In an attempt to catch failures in starting the packager on first attempt,
307// wait for 300 ms before resolving the promise
b9356af0Meena Kunnathur Balakrishnan10 years ago308Q.delay(300).done(() => deferred.resolve(void 0));
32cab018Meena Kunnathur Balakrishnan10 years ago309return deferred.promise;
4677921cdigeff10 years ago310}
311
312private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago313return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago314}
315
ea8a5f88digeff10 years ago316private onSocketOpened() {
7cc67271digeff10 years ago317this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
d124bf0eYuri Skorokhodov7 years ago318logger.log(localize("EstablishedConnectionWithPackagerToReactNativeApp", "Established a connection with the Proxy (Packager) to the React Native application")));
4677921cdigeff10 years ago319}
320
9174feb7Vladimir Kotikov9 years ago321private killWorker() {
322if (!this.singleLifetimeWorker) return;
323this.singleLifetimeWorker.stop();
324this.singleLifetimeWorker = null;
325}
e7b314e8Vladimir Kotikov9 years ago326
9174feb7Vladimir Kotikov9 years ago327private onMessage(message: string) {
5d4d4de0digeff10 years ago328try {
0a68f8dbArtem Egorov8 years ago329logger.verbose("From RN APP: " + message);
ea8a5f88digeff10 years ago330let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago331if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago332// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
333// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago334this.killWorker();
5d4d4de0digeff10 years ago335// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
336this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago337} else if (object.method === "$disconnected") {
338// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago339this.killWorker();
5d4d4de0digeff10 years ago340} else if (object.method) {
341// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago342if (this.singleLifetimeWorker) {
343this.singleLifetimeWorker.postMessage(object);
344}
5d4d4de0digeff10 years ago345} else {
ea8a5f88digeff10 years ago346// 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 ago347logger.verbose(`The react-native app sent a message without specifying a method: ${message}`);
5d4d4de0digeff10 years ago348}
349} catch (exception) {
1758f9a6Yuri Skorokhodov7 years ago350printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToProcessMessageFromReactNativeApp, message), exception);
4677921cdigeff10 years ago351}
352}
353
354private gotPrepareJSRuntime(message: any): void {
355// Create the sandbox, and replay that we finished processing the message
ff7dce65digeff10 years ago356this.startNewWorkerLifetime().done(() => {
357this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
1758f9a6Yuri Skorokhodov7 years ago358}, error => printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToPrepareJSRuntimeEnvironment, message), error));
4677921cdigeff10 years ago359}
360
ff7dce65digeff10 years ago361private sendMessageToApp(message: any): void {
5c8365a6Artem Egorov8 years ago362let stringified: string = "";
354c28a1digeff10 years ago363try {
364stringified = JSON.stringify(message);
d124bf0eYuri Skorokhodov7 years ago365logger.verbose(`To RN APP: ${stringified}`);
354c28a1digeff10 years ago366this.socketToApp.send(stringified);
367} catch (exception) {
368let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
1758f9a6Yuri Skorokhodov7 years ago369printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToSendMessageToTheReactNativeApp, messageToShow), exception);
354c28a1digeff10 years ago370}
4677921cdigeff10 years ago371}
372}