microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.16.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

394lines · 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};
0864d702Ruslan Bikkinin7 years ago50// Prevent leaking process.versions from debugger process to
51// worker because pure React Native doesn't do that and some packages as js-md5 rely on this behavior
52Object.defineProperty(process, "versions", {
53value: undefined
54});
7daed3fcArtem Egorov8 years ago55
4cf8fdf4Yuri Skorokhodov6 years ago56// TODO: Replace by url.fileURLToPath method when Node 10 LTS become deprecated
57function fileUrlToPath(url) {
58if (process.platform === 'win32') {
59return url.toString().replace('file:///', '');
60} else {
61return url.toString().replace('file://', '');
62}
63}
64
2ecfbd20Yuri Skorokhodov7 years ago65function getNativeModules() {
66var NativeModules;
67try {
68// This approach is for old RN versions
69NativeModules = global.require('NativeModules');
70} catch (err) {
71// ignore error and try another way for more recent RN versions
72try {
73var nativeModuleId;
74var modules = global.__r.getModules();
75var ids = Object.keys(modules);
76for (var i = 0; i < ids.length; i++) {
77if (modules[ids[i]].verboseName) {
78var packagePath = new String(modules[ids[i]].verboseName);
4cf8fdf4Yuri Skorokhodov6 years ago79if (packagePath.indexOf('react-native/Libraries/BatchedBridge/NativeModules.js') > 0) {
2ecfbd20Yuri Skorokhodov7 years ago80nativeModuleId = parseInt(ids[i], 10);
81break;
82}
83}
84}
85if (nativeModuleId) {
86NativeModules = global.__r(nativeModuleId);
87}
88}
89catch (err) {
90// suppress errors
91}
92}
93return NativeModules;
94}
95
96// Originally, this was made for iOS only
7daed3fcArtem Egorov8 years ago97var vscodeHandlers = {
98'vscode_reloadApp': function () {
2ecfbd20Yuri Skorokhodov7 years ago99var NativeModules = getNativeModules();
100if (NativeModules) {
101NativeModules.DevMenu.reload();
7daed3fcArtem Egorov8 years ago102}
103},
104'vscode_showDevMenu': function () {
2ecfbd20Yuri Skorokhodov7 years ago105var NativeModules = getNativeModules();
106if (NativeModules) {
107NativeModules.DevMenu.show();
7daed3fcArtem Egorov8 years ago108}
109}
110};
111
112process.on("message", function (message) {
113if (message.data && vscodeHandlers[message.data.method]) {
114vscodeHandlers[message.data.method]();
115} else if(onmessage) {
116onmessage(message);
117}
cc70057dVladimir Kotikov9 years ago118});
7daed3fcArtem Egorov8 years ago119
cc70057dVladimir Kotikov9 years ago120var postMessage = function(message){
121process.send(message);
122};
bb869343Serge Svekolnikov8 years ago123
124if (!self.postMessage) {
125self.postMessage = postMessage;
126}
127
cc70057dVladimir Kotikov9 years ago128var importScripts = (function(){
129var fs=require('fs'), vm=require('vm');
130return function(scriptUrl){
4cf8fdf4Yuri Skorokhodov6 years ago131scriptUrl = fileUrlToPath(scriptUrl);
132var scriptCode = fs.readFileSync(scriptUrl, 'utf8');
e67ace8aYuri Skorokhodov6 years ago133// Add a 'debugger;' statement to stop code execution
134// to wait for the sourcemaps to be processed by the debug adapter
135vm.runInThisContext('debugger;' + scriptCode, {filename: scriptUrl});
cc70057dVladimir Kotikov9 years ago136};
4cf8fdf4Yuri Skorokhodov6 years ago137})();
138`;
cc70057dVladimir Kotikov9 years ago139
cf6dd6b9Yuri Skorokhodov7 years ago140public static CONSOLE_TRACE_PATCH = `// Worker is ran as nodejs process, so console.trace() writes to stderr and it leads to error in native app
141// To avoid this console.trace() is overridden to print stacktrace via console.log()
142// Please, see Node JS implementation: https://github.com/nodejs/node/blob/master/lib/internal/console/constructor.js
143console.trace = (function() {
144return function() {
145try {
146var err = {
147name: 'Trace',
148message: require('util').format.apply(null, arguments)
149};
150// Node uses 10, but usually it's not enough for RN app trace
151Error.stackTraceLimit = 30;
152Error.captureStackTrace(err, console.trace);
153console.log(err.stack);
154} catch (e) {
155console.error(e);
156}
157};
4cf8fdf4Yuri Skorokhodov6 years ago158})();
159`;
cf6dd6b9Yuri Skorokhodov7 years ago160
13a99427Yuri Skorokhodov6 years ago161public static PROCESS_TO_STRING_PATCH = `// As worker is ran in node, it breaks broadcast-channels package approach of identifying if it’s ran in node:
162// https://github.com/pubkey/broadcast-channel/blob/master/src/util.js#L64
163// To avoid it if process.toString() is called if will return empty string instead of [object process].
164var nativeObjectToString = Object.prototype.toString;
165Object.prototype.toString = function() {
166if (this === process) {
167return '';
168} else {
169return nativeObjectToString.call(this);
170}
f4e3ce39Yuri Skorokhodov6 years ago171};
13a99427Yuri Skorokhodov6 years ago172`;
173
039239d1Vladimir Kotikov9 years ago174public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago175// and started listening for IPC messages
176postMessage({workerLoaded:true});`;
177
bb869343Serge Svekolnikov8 years ago178public static FETCH_STUB = `(function(self) {
4cf8fdf4Yuri Skorokhodov6 years ago179'use strict';
bb869343Serge Svekolnikov8 years ago180
4cf8fdf4Yuri Skorokhodov6 years ago181if (self.fetch) {
182return;
183}
184
185self.fetch = fetch;
bb869343Serge Svekolnikov8 years ago186
4cf8fdf4Yuri Skorokhodov6 years ago187function fetch(url) {
188return new Promise((resolve, reject) => {
189var data = require('fs').readFileSync(fileUrlToPath(url), 'utf8');
190resolve(
191{
192text: function () {
193return data;
194}
bb869343Serge Svekolnikov8 years ago195});
4cf8fdf4Yuri Skorokhodov6 years ago196});
197}
198})(global);
199`;
bb869343Serge Svekolnikov8 years ago200
6eeec3c0Serge Svekolnikov8 years ago201private packagerAddress: string;
e3ae4227digeff10 years ago202private packagerPort: number;
4677921cdigeff10 years ago203private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago204private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago205private packagerRemoteRoot?: string;
206private packagerLocalRoot?: string;
cf911877Yuri Skorokhodov7 years ago207private debuggerWorkerUrlPath?: string;
ea8a5f88digeff10 years ago208private socketToApp: WebSocket;
5c8365a6Artem Egorov8 years ago209private singleLifetimeWorker: IDebuggeeWorker | null;
3b6023b2Jimmy Thomson10 years ago210private webSocketConstructor: (url: string) => WebSocket;
211
7cc67271digeff10 years ago212private executionLimiter = new ExecutionsLimiter();
cc70057dVladimir Kotikov9 years ago213private nodeFileSystem = new NodeFileSystem();
214private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago215
6eeec3c0Serge Svekolnikov8 years ago216constructor(
217attachRequestArguments: any,
218sourcesStoragePath: string,
219projectRootPath: string,
220{
221webSocketConstructor = (url: string) => new WebSocket(url),
222} = {}) {
e45838cbVladimir Kotikov9 years ago223super();
6eeec3c0Serge Svekolnikov8 years ago224this.packagerAddress = attachRequestArguments.address || "localhost";
225this.packagerPort = attachRequestArguments.port;
226this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
227this.packagerLocalRoot = attachRequestArguments.localRoot;
cf911877Yuri Skorokhodov7 years ago228this.debuggerWorkerUrlPath = attachRequestArguments.debuggerWorkerUrlPath;
4677921cdigeff10 years ago229this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago230this.projectRootPath = projectRootPath;
1758f9a6Yuri Skorokhodov7 years ago231if (!this.sourcesStoragePath)
232throw ErrorHelper.getInternalError(InternalErrorCode.SourcesStoragePathIsNullOrEmpty);
3b6023b2Jimmy Thomson10 years ago233this.webSocketConstructor = webSocketConstructor;
6eeec3c0Serge Svekolnikov8 years ago234this.scriptImporter = new ScriptImporter(this.packagerAddress, this.packagerPort, sourcesStoragePath, this.packagerRemoteRoot, this.packagerLocalRoot);
4677921cdigeff10 years ago235}
236
cc70057dVladimir Kotikov9 years ago237public start(retryAttempt: boolean = false): Q.Promise<any> {
d124bf0eYuri Skorokhodov7 years ago238const errPackagerNotRunning = ErrorHelper.getInternalError(InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort, this.packagerPort);
0a68f8dbArtem Egorov8 years ago239
6eeec3c0Serge Svekolnikov8 years ago240return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
cc70057dVladimir Kotikov9 years ago241.then(() => {
242// Don't fetch debugger worker on socket disconnect
243return retryAttempt ? Q.resolve<void>(void 0) :
244this.downloadAndPatchDebuggerWorker();
245})
246.then(() => this.createSocketToApp(retryAttempt));
ff7dce65digeff10 years ago247}
248
e45838cbVladimir Kotikov9 years ago249public stop() {
250if (this.socketToApp) {
251this.socketToApp.removeAllListeners();
252this.socketToApp.close();
253}
254
255if (this.singleLifetimeWorker) {
256this.singleLifetimeWorker.stop();
257}
258}
259
cc70057dVladimir Kotikov9 years ago260public downloadAndPatchDebuggerWorker(): Q.Promise<void> {
261let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
cf911877Yuri Skorokhodov7 years ago262return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath, this.projectRootPath, this.debuggerWorkerUrlPath)
cc70057dVladimir Kotikov9 years ago263.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
264.then((workerContent: string) => {
bb869343Serge Svekolnikov8 years ago265const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
cc70057dVladimir Kotikov9 years ago266// Add our customizations to debugger worker to get it working smoothly
267// in Node env and polyfill WebWorkers API over Node's IPC.
bb869343Serge Svekolnikov8 years ago268const modifiedDebuggeeContent = [
269MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
cf6dd6b9Yuri Skorokhodov7 years ago270MultipleLifetimesAppWorker.CONSOLE_TRACE_PATCH,
13a99427Yuri Skorokhodov6 years ago271MultipleLifetimesAppWorker.PROCESS_TO_STRING_PATCH,
bb869343Serge Svekolnikov8 years ago272isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
273workerContent,
274MultipleLifetimesAppWorker.WORKER_DONE,
275].join("\n");
cc70057dVladimir Kotikov9 years ago276return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
277});
278}
279
ff7dce65digeff10 years ago280private startNewWorkerLifetime(): Q.Promise<void> {
6eeec3c0Serge Svekolnikov8 years ago281this.singleLifetimeWorker = new ForkedAppWorker(this.packagerAddress, this.packagerPort, this.sourcesStoragePath, this.projectRootPath,
282(message) => {
283this.sendMessageToApp(message);
284},
285this.packagerRemoteRoot, this.packagerLocalRoot);
0a68f8dbArtem Egorov8 years ago286logger.verbose("A new app worker lifetime was created.");
e45838cbVladimir Kotikov9 years ago287return this.singleLifetimeWorker.start()
cc70057dVladimir Kotikov9 years ago288.then(startedEvent => {
289this.emit("connected", startedEvent);
290});
4677921cdigeff10 years ago291}
292
cc70057dVladimir Kotikov9 years ago293private createSocketToApp(retryAttempt: boolean = false): Q.Promise<void> {
b9356af0Meena Kunnathur Balakrishnan10 years ago294let deferred = Q.defer<void>();
6e731058Jimmy Thomson10 years ago295this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
b9356af0Meena Kunnathur Balakrishnan10 years ago296this.socketToApp.on("open", () => {
297this.onSocketOpened();
298});
299b0557Patricio Beltran10 years ago299this.socketToApp.on("close",
300() => {
301this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
302/*
303* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
304* it closes the socket because it already has a connection to a debugger.
305* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
306*/
6d5c8798Nikita Matrosov9 years ago307let msgKey = "_closeMessage";
308if (this.socketToApp[msgKey] === "Another debugger is already connected") {
d124bf0eYuri Skorokhodov7 years ago309deferred.reject(ErrorHelper.getInternalError(InternalErrorCode.AnotherDebuggerConnectedToPackager));
299b0557Patricio Beltran10 years ago310}
d124bf0eYuri Skorokhodov7 years ago311logger.log(localize("DisconnectedFromThePackagerToReactNative", "Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon..."));
299b0557Patricio Beltran10 years ago312});
313setTimeout(() => {
314this.start(true /* retryAttempt */);
315}, 100);
316});
b9356af0Meena Kunnathur Balakrishnan10 years ago317this.socketToApp.on("message",
ea8a5f88digeff10 years ago318(message: any) => this.onMessage(message));
b9356af0Meena Kunnathur Balakrishnan10 years ago319this.socketToApp.on("error",
32cab018Meena Kunnathur Balakrishnan10 years ago320(error: Error) => {
cc70057dVladimir Kotikov9 years ago321if (retryAttempt) {
1758f9a6Yuri Skorokhodov7 years ago322printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.ReconnectionToPackagerFailedCheckForErrorsOrRestartReactNative), error);
499fe4ebMeena Kunnathur Balakrishnan10 years ago323}
324
32cab018Meena Kunnathur Balakrishnan10 years ago325deferred.reject(error);
326});
327
499fe4ebMeena Kunnathur Balakrishnan10 years ago328// In an attempt to catch failures in starting the packager on first attempt,
329// wait for 300 ms before resolving the promise
b9356af0Meena Kunnathur Balakrishnan10 years ago330Q.delay(300).done(() => deferred.resolve(void 0));
32cab018Meena Kunnathur Balakrishnan10 years ago331return deferred.promise;
4677921cdigeff10 years ago332}
333
334private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago335return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago336}
337
ea8a5f88digeff10 years ago338private onSocketOpened() {
7cc67271digeff10 years ago339this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
d124bf0eYuri Skorokhodov7 years ago340logger.log(localize("EstablishedConnectionWithPackagerToReactNativeApp", "Established a connection with the Proxy (Packager) to the React Native application")));
4677921cdigeff10 years ago341}
342
9174feb7Vladimir Kotikov9 years ago343private killWorker() {
344if (!this.singleLifetimeWorker) return;
345this.singleLifetimeWorker.stop();
346this.singleLifetimeWorker = null;
347}
e7b314e8Vladimir Kotikov9 years ago348
9174feb7Vladimir Kotikov9 years ago349private onMessage(message: string) {
5d4d4de0digeff10 years ago350try {
0a68f8dbArtem Egorov8 years ago351logger.verbose("From RN APP: " + message);
ea8a5f88digeff10 years ago352let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago353if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago354// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
355// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago356this.killWorker();
5d4d4de0digeff10 years ago357// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
358this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago359} else if (object.method === "$disconnected") {
360// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago361this.killWorker();
5d4d4de0digeff10 years ago362} else if (object.method) {
363// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago364if (this.singleLifetimeWorker) {
365this.singleLifetimeWorker.postMessage(object);
366}
5d4d4de0digeff10 years ago367} else {
ea8a5f88digeff10 years ago368// 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 ago369logger.verbose(`The react-native app sent a message without specifying a method: ${message}`);
5d4d4de0digeff10 years ago370}
371} catch (exception) {
1758f9a6Yuri Skorokhodov7 years ago372printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToProcessMessageFromReactNativeApp, message), exception);
4677921cdigeff10 years ago373}
374}
375
376private gotPrepareJSRuntime(message: any): void {
377// Create the sandbox, and replay that we finished processing the message
ff7dce65digeff10 years ago378this.startNewWorkerLifetime().done(() => {
379this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
1758f9a6Yuri Skorokhodov7 years ago380}, error => printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToPrepareJSRuntimeEnvironment, message), error));
4677921cdigeff10 years ago381}
382
ff7dce65digeff10 years ago383private sendMessageToApp(message: any): void {
5c8365a6Artem Egorov8 years ago384let stringified: string = "";
354c28a1digeff10 years ago385try {
386stringified = JSON.stringify(message);
d124bf0eYuri Skorokhodov7 years ago387logger.verbose(`To RN APP: ${stringified}`);
354c28a1digeff10 years ago388this.socketToApp.send(stringified);
389} catch (exception) {
390let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
1758f9a6Yuri Skorokhodov7 years ago391printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToSendMessageToTheReactNativeApp, messageToShow), exception);
354c28a1digeff10 years ago392}
4677921cdigeff10 years ago393}
394}