microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.1.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

411lines · 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
cc70057dVladimir Kotikov9 years ago4import * as path from "path";
ea8a5f88digeff10 years ago5import * as WebSocket from "ws";
e45838cbVladimir Kotikov9 years ago6import { EventEmitter } from "events";
0a68f8dbArtem Egorov8 years ago7import { ensurePackagerRunning } from "../common/packagerStatus";
a4a7e387Meena Kunnathur Balakrishnan10 years ago8import {ErrorHelper} from "../common/error/errorHelper";
a6562589RedMickey6 years ago9import { logger } from "vscode-debugadapter";
7cc67271digeff10 years ago10import {ExecutionsLimiter} from "../common/executionsLimiter";
b05b5086Vladimir Kotikov9 years ago11import { ForkedAppWorker } from "./forkedAppWorker";
cc70057dVladimir Kotikov9 years ago12import { ScriptImporter } from "./scriptImporter";
bb869343Serge Svekolnikov8 years ago13import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
d124bf0eYuri Skorokhodov7 years ago14import * as nls from "vscode-nls";
15import { InternalErrorCode } from "../common/error/internalErrorCode";
ce5e88eeYuri Skorokhodov5 years ago16import { FileSystem } from "../common/node/fileSystem";
17import { PromiseUtil } from "../common/node/promise";
2d8af448Yuri Skorokhodov6 years ago18nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
d124bf0eYuri Skorokhodov7 years ago19const localize = nls.loadMessageBundle();
4677921cdigeff10 years ago20
e45838cbVladimir Kotikov9 years ago21export interface RNAppMessage {
ea8a5f88digeff10 years ago22method: string;
e45838cbVladimir Kotikov9 years ago23url?: string;
ea8a5f88digeff10 years ago24// These objects have also other properties but that we don't currently use
25}
5d4d4de0digeff10 years ago26
e45838cbVladimir Kotikov9 years ago27export interface IDebuggeeWorker {
ce5e88eeYuri Skorokhodov5 years ago28start(): Promise<any>;
e45838cbVladimir Kotikov9 years ago29stop(): void;
30postMessage(message: RNAppMessage): void;
31}
32
1758f9a6Yuri Skorokhodov7 years ago33function printDebuggingError(error: Error, reason: any) {
34const nestedError = ErrorHelper.getNestedError(error, InternalErrorCode.DebuggingWontWorkReloadJSAndReconnect, reason);
0a68f8dbArtem Egorov8 years ago35
36logger.error(nestedError.message);
039239d1Vladimir Kotikov9 years ago37}
38
39/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
40* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
41* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
42* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
43*/
44
45export class MultipleLifetimesAppWorker extends EventEmitter {
46public static WORKER_BOOTSTRAP = `
cc70057dVladimir Kotikov9 years ago47// Initialize some variables before react-native code would access them
d64a6928Vladimir Kotikov9 years ago48var onmessage=null, self=global;
cc70057dVladimir Kotikov9 years ago49// Cache Node's original require as __debug__.require
d64a6928Vladimir Kotikov9 years ago50global.__debug__={require: require};
0864d702Ruslan Bikkinin7 years ago51// Prevent leaking process.versions from debugger process to
52// worker because pure React Native doesn't do that and some packages as js-md5 rely on this behavior
53Object.defineProperty(process, "versions", {
54value: undefined
55});
7daed3fcArtem Egorov8 years ago56
4cf8fdf4Yuri Skorokhodov6 years ago57// TODO: Replace by url.fileURLToPath method when Node 10 LTS become deprecated
58function fileUrlToPath(url) {
59if (process.platform === 'win32') {
60return url.toString().replace('file:///', '');
61} else {
62return url.toString().replace('file://', '');
63}
64}
65
2ecfbd20Yuri Skorokhodov7 years ago66function getNativeModules() {
67var NativeModules;
68try {
69// This approach is for old RN versions
70NativeModules = global.require('NativeModules');
71} catch (err) {
72// ignore error and try another way for more recent RN versions
73try {
74var nativeModuleId;
75var modules = global.__r.getModules();
76var ids = Object.keys(modules);
77for (var i = 0; i < ids.length; i++) {
78if (modules[ids[i]].verboseName) {
79var packagePath = new String(modules[ids[i]].verboseName);
4cf8fdf4Yuri Skorokhodov6 years ago80if (packagePath.indexOf('react-native/Libraries/BatchedBridge/NativeModules.js') > 0) {
2ecfbd20Yuri Skorokhodov7 years ago81nativeModuleId = parseInt(ids[i], 10);
82break;
83}
84}
85}
86if (nativeModuleId) {
87NativeModules = global.__r(nativeModuleId);
88}
89}
90catch (err) {
91// suppress errors
92}
93}
94return NativeModules;
95}
96
97// Originally, this was made for iOS only
7daed3fcArtem Egorov8 years ago98var vscodeHandlers = {
99'vscode_reloadApp': function () {
2ecfbd20Yuri Skorokhodov7 years ago100var NativeModules = getNativeModules();
4dfc9ffdRedMickey5 years ago101if (NativeModules && NativeModules.DevMenu) {
2ecfbd20Yuri Skorokhodov7 years ago102NativeModules.DevMenu.reload();
7daed3fcArtem Egorov8 years ago103}
104},
105'vscode_showDevMenu': function () {
2ecfbd20Yuri Skorokhodov7 years ago106var NativeModules = getNativeModules();
4dfc9ffdRedMickey5 years ago107if (NativeModules && NativeModules.DevMenu) {
2ecfbd20Yuri Skorokhodov7 years ago108NativeModules.DevMenu.show();
7daed3fcArtem Egorov8 years ago109}
110}
111};
112
113process.on("message", function (message) {
114if (message.data && vscodeHandlers[message.data.method]) {
115vscodeHandlers[message.data.method]();
116} else if(onmessage) {
117onmessage(message);
118}
cc70057dVladimir Kotikov9 years ago119});
7daed3fcArtem Egorov8 years ago120
cc70057dVladimir Kotikov9 years ago121var postMessage = function(message){
122process.send(message);
123};
bb869343Serge Svekolnikov8 years ago124
125if (!self.postMessage) {
126self.postMessage = postMessage;
127}
128
cc70057dVladimir Kotikov9 years ago129var importScripts = (function(){
130var fs=require('fs'), vm=require('vm');
131return function(scriptUrl){
4cf8fdf4Yuri Skorokhodov6 years ago132scriptUrl = fileUrlToPath(scriptUrl);
133var scriptCode = fs.readFileSync(scriptUrl, 'utf8');
e67ace8aYuri Skorokhodov6 years ago134// Add a 'debugger;' statement to stop code execution
135// to wait for the sourcemaps to be processed by the debug adapter
136vm.runInThisContext('debugger;' + scriptCode, {filename: scriptUrl});
cc70057dVladimir Kotikov9 years ago137};
4cf8fdf4Yuri Skorokhodov6 years ago138})();
139`;
cc70057dVladimir Kotikov9 years ago140
cf6dd6b9Yuri Skorokhodov7 years ago141public static CONSOLE_TRACE_PATCH = `// Worker is ran as nodejs process, so console.trace() writes to stderr and it leads to error in native app
142// To avoid this console.trace() is overridden to print stacktrace via console.log()
143// Please, see Node JS implementation: https://github.com/nodejs/node/blob/master/lib/internal/console/constructor.js
144console.trace = (function() {
145return function() {
146try {
147var err = {
148name: 'Trace',
149message: require('util').format.apply(null, arguments)
150};
151// Node uses 10, but usually it's not enough for RN app trace
152Error.stackTraceLimit = 30;
153Error.captureStackTrace(err, console.trace);
154console.log(err.stack);
155} catch (e) {
156console.error(e);
157}
158};
4cf8fdf4Yuri Skorokhodov6 years ago159})();
160`;
cf6dd6b9Yuri Skorokhodov7 years ago161
13a99427Yuri Skorokhodov6 years ago162public 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:
163// https://github.com/pubkey/broadcast-channel/blob/master/src/util.js#L64
164// To avoid it if process.toString() is called if will return empty string instead of [object process].
165var nativeObjectToString = Object.prototype.toString;
166Object.prototype.toString = function() {
167if (this === process) {
168return '';
169} else {
170return nativeObjectToString.call(this);
171}
f4e3ce39Yuri Skorokhodov6 years ago172};
13a99427Yuri Skorokhodov6 years ago173`;
174
039239d1Vladimir Kotikov9 years ago175public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago176// and started listening for IPC messages
177postMessage({workerLoaded:true});`;
178
bb869343Serge Svekolnikov8 years ago179public static FETCH_STUB = `(function(self) {
4cf8fdf4Yuri Skorokhodov6 years ago180'use strict';
bb869343Serge Svekolnikov8 years ago181
4cf8fdf4Yuri Skorokhodov6 years ago182if (self.fetch) {
183return;
184}
185
186self.fetch = fetch;
bb869343Serge Svekolnikov8 years ago187
4cf8fdf4Yuri Skorokhodov6 years ago188function fetch(url) {
189return new Promise((resolve, reject) => {
190var data = require('fs').readFileSync(fileUrlToPath(url), 'utf8');
191resolve(
192{
193text: function () {
194return data;
195}
bb869343Serge Svekolnikov8 years ago196});
4cf8fdf4Yuri Skorokhodov6 years ago197});
198}
199})(global);
200`;
bb869343Serge Svekolnikov8 years ago201
6eeec3c0Serge Svekolnikov8 years ago202private packagerAddress: string;
e3ae4227digeff10 years ago203private packagerPort: number;
4677921cdigeff10 years ago204private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago205private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago206private packagerRemoteRoot?: string;
207private packagerLocalRoot?: string;
cf911877Yuri Skorokhodov7 years ago208private debuggerWorkerUrlPath?: string;
ea8a5f88digeff10 years ago209private socketToApp: WebSocket;
5c8365a6Artem Egorov8 years ago210private singleLifetimeWorker: IDebuggeeWorker | null;
3b6023b2Jimmy Thomson10 years ago211private webSocketConstructor: (url: string) => WebSocket;
212
7cc67271digeff10 years ago213private executionLimiter = new ExecutionsLimiter();
ce5e88eeYuri Skorokhodov5 years ago214private nodeFileSystem = new FileSystem();
cc70057dVladimir Kotikov9 years ago215private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago216
6eeec3c0Serge Svekolnikov8 years ago217constructor(
218attachRequestArguments: any,
219sourcesStoragePath: string,
220projectRootPath: string,
221{
222webSocketConstructor = (url: string) => new WebSocket(url),
223} = {}) {
e45838cbVladimir Kotikov9 years ago224super();
6eeec3c0Serge Svekolnikov8 years ago225this.packagerAddress = attachRequestArguments.address || "localhost";
226this.packagerPort = attachRequestArguments.port;
227this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
228this.packagerLocalRoot = attachRequestArguments.localRoot;
cf911877Yuri Skorokhodov7 years ago229this.debuggerWorkerUrlPath = attachRequestArguments.debuggerWorkerUrlPath;
4677921cdigeff10 years ago230this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago231this.projectRootPath = projectRootPath;
1758f9a6Yuri Skorokhodov7 years ago232if (!this.sourcesStoragePath)
233throw ErrorHelper.getInternalError(InternalErrorCode.SourcesStoragePathIsNullOrEmpty);
3b6023b2Jimmy Thomson10 years ago234this.webSocketConstructor = webSocketConstructor;
6eeec3c0Serge Svekolnikov8 years ago235this.scriptImporter = new ScriptImporter(this.packagerAddress, this.packagerPort, sourcesStoragePath, this.packagerRemoteRoot, this.packagerLocalRoot);
4677921cdigeff10 years ago236}
237
ce5e88eeYuri Skorokhodov5 years ago238public start(retryAttempt: boolean = false): Promise<any> {
d124bf0eYuri Skorokhodov7 years ago239const errPackagerNotRunning = ErrorHelper.getInternalError(InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort, this.packagerPort);
0a68f8dbArtem Egorov8 years ago240
6eeec3c0Serge Svekolnikov8 years ago241return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
cc70057dVladimir Kotikov9 years ago242.then(() => {
243// Don't fetch debugger worker on socket disconnect
ce5e88eeYuri Skorokhodov5 years ago244return retryAttempt ? Promise.resolve() :
cc70057dVladimir Kotikov9 years ago245this.downloadAndPatchDebuggerWorker();
246})
247.then(() => this.createSocketToApp(retryAttempt));
ff7dce65digeff10 years ago248}
249
e45838cbVladimir Kotikov9 years ago250public stop() {
251if (this.socketToApp) {
252this.socketToApp.removeAllListeners();
253this.socketToApp.close();
254}
255
256if (this.singleLifetimeWorker) {
257this.singleLifetimeWorker.stop();
258}
259}
260
ce5e88eeYuri Skorokhodov5 years ago261public downloadAndPatchDebuggerWorker(): Promise<void> {
cc70057dVladimir Kotikov9 years ago262let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
cf911877Yuri Skorokhodov7 years ago263return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath, this.projectRootPath, this.debuggerWorkerUrlPath)
cc70057dVladimir Kotikov9 years ago264.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
265.then((workerContent: string) => {
bb869343Serge Svekolnikov8 years ago266const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
cc70057dVladimir Kotikov9 years ago267// Add our customizations to debugger worker to get it working smoothly
268// in Node env and polyfill WebWorkers API over Node's IPC.
bb869343Serge Svekolnikov8 years ago269const modifiedDebuggeeContent = [
270MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
cf6dd6b9Yuri Skorokhodov7 years ago271MultipleLifetimesAppWorker.CONSOLE_TRACE_PATCH,
13a99427Yuri Skorokhodov6 years ago272MultipleLifetimesAppWorker.PROCESS_TO_STRING_PATCH,
bb869343Serge Svekolnikov8 years ago273isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
274workerContent,
275MultipleLifetimesAppWorker.WORKER_DONE,
276].join("\n");
cc70057dVladimir Kotikov9 years ago277return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
278});
279}
280
7e74daf7Yuri Skorokhodov6 years ago281public showDevMenuCommand(): void {
282if (this.singleLifetimeWorker) {
283this.singleLifetimeWorker.postMessage({
284method: "vscode_showDevMenu",
285});
286}
287}
288
289public reloadAppCommand(): void {
290if (this.singleLifetimeWorker) {
291this.singleLifetimeWorker.postMessage({
292method: "vscode_reloadApp",
293});
294}
295}
296
ce5e88eeYuri Skorokhodov5 years ago297private startNewWorkerLifetime(): Promise<void> {
6eeec3c0Serge Svekolnikov8 years ago298this.singleLifetimeWorker = new ForkedAppWorker(this.packagerAddress, this.packagerPort, this.sourcesStoragePath, this.projectRootPath,
299(message) => {
300this.sendMessageToApp(message);
301},
302this.packagerRemoteRoot, this.packagerLocalRoot);
0a68f8dbArtem Egorov8 years ago303logger.verbose("A new app worker lifetime was created.");
e45838cbVladimir Kotikov9 years ago304return this.singleLifetimeWorker.start()
cc70057dVladimir Kotikov9 years ago305.then(startedEvent => {
306this.emit("connected", startedEvent);
307});
4677921cdigeff10 years ago308}
309
ce5e88eeYuri Skorokhodov5 years ago310private createSocketToApp(retryAttempt: boolean = false): Promise<void> {
311return new Promise((resolve, reject) => {
312this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
313this.socketToApp.on("open", () => {
314this.onSocketOpened();
299b0557Patricio Beltran10 years ago315});
ce5e88eeYuri Skorokhodov5 years ago316this.socketToApp.on("close",
317() => {
318this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
319/*
320* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
321* it closes the socket because it already has a connection to a debugger.
322* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
323*/
324let msgKey = "_closeMessage";
325if (this.socketToApp[msgKey] === "Another debugger is already connected") {
326reject(ErrorHelper.getInternalError(InternalErrorCode.AnotherDebuggerConnectedToPackager));
327}
328logger.log(localize("DisconnectedFromThePackagerToReactNative", "Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon..."));
329});
330setTimeout(() => {
331this.start(true /* retryAttempt */);
332}, 100);
333});
334this.socketToApp.on("message",
335(message: any) => this.onMessage(message));
336this.socketToApp.on("error",
337(error: Error) => {
338if (retryAttempt) {
339printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.ReconnectionToPackagerFailedCheckForErrorsOrRestartReactNative), error);
340}
499fe4ebMeena Kunnathur Balakrishnan10 years ago341
ce5e88eeYuri Skorokhodov5 years ago342reject(error);
343});
32cab018Meena Kunnathur Balakrishnan10 years ago344
ce5e88eeYuri Skorokhodov5 years ago345// In an attempt to catch failures in starting the packager on first attempt,
346// wait for 300 ms before resolving the promise
259c018fYuri Skorokhodov5 years ago347PromiseUtil.delay(300).then(() => resolve());
ce5e88eeYuri Skorokhodov5 years ago348});
4677921cdigeff10 years ago349}
350
351private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago352return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago353}
354
ea8a5f88digeff10 years ago355private onSocketOpened() {
7cc67271digeff10 years ago356this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
d124bf0eYuri Skorokhodov7 years ago357logger.log(localize("EstablishedConnectionWithPackagerToReactNativeApp", "Established a connection with the Proxy (Packager) to the React Native application")));
4677921cdigeff10 years ago358}
359
9174feb7Vladimir Kotikov9 years ago360private killWorker() {
361if (!this.singleLifetimeWorker) return;
362this.singleLifetimeWorker.stop();
363this.singleLifetimeWorker = null;
364}
e7b314e8Vladimir Kotikov9 years ago365
9174feb7Vladimir Kotikov9 years ago366private onMessage(message: string) {
5d4d4de0digeff10 years ago367try {
0a68f8dbArtem Egorov8 years ago368logger.verbose("From RN APP: " + message);
ea8a5f88digeff10 years ago369let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago370if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago371// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
372// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago373this.killWorker();
5d4d4de0digeff10 years ago374// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
375this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago376} else if (object.method === "$disconnected") {
377// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago378this.killWorker();
5d4d4de0digeff10 years ago379} else if (object.method) {
380// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago381if (this.singleLifetimeWorker) {
382this.singleLifetimeWorker.postMessage(object);
383}
5d4d4de0digeff10 years ago384} else {
ea8a5f88digeff10 years ago385// 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 ago386logger.verbose(`The react-native app sent a message without specifying a method: ${message}`);
5d4d4de0digeff10 years ago387}
388} catch (exception) {
1758f9a6Yuri Skorokhodov7 years ago389printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToProcessMessageFromReactNativeApp, message), exception);
4677921cdigeff10 years ago390}
391}
392
393private gotPrepareJSRuntime(message: any): void {
394// Create the sandbox, and replay that we finished processing the message
ce5e88eeYuri Skorokhodov5 years ago395this.startNewWorkerLifetime().then(() => {
ff7dce65digeff10 years ago396this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
1758f9a6Yuri Skorokhodov7 years ago397}, error => printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToPrepareJSRuntimeEnvironment, message), error));
4677921cdigeff10 years ago398}
399
ff7dce65digeff10 years ago400private sendMessageToApp(message: any): void {
5c8365a6Artem Egorov8 years ago401let stringified: string = "";
354c28a1digeff10 years ago402try {
403stringified = JSON.stringify(message);
d124bf0eYuri Skorokhodov7 years ago404logger.verbose(`To RN APP: ${stringified}`);
354c28a1digeff10 years ago405this.socketToApp.send(stringified);
406} catch (exception) {
407let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
1758f9a6Yuri Skorokhodov7 years ago408printDebuggingError(ErrorHelper.getInternalError(InternalErrorCode.FailedToSendMessageToTheReactNativeApp, messageToShow), exception);
354c28a1digeff10 years ago409}
4677921cdigeff10 years ago410}
411}