microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.10.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

483lines · 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";
09f6024fHeniker4 years ago5import { EventEmitter } from "events";
5940f996RedMickey5 years ago6import * as vscode from "vscode";
ea8a5f88digeff10 years ago7import * as WebSocket from "ws";
09f6024fHeniker4 years ago8import { logger } from "vscode-debugadapter";
9import * as nls from "vscode-nls";
0a68f8dbArtem Egorov8 years ago10import { ensurePackagerRunning } from "../common/packagerStatus";
34472878RedMickey5 years ago11import { ErrorHelper } from "../common/error/errorHelper";
12import { ExecutionsLimiter } from "../common/executionsLimiter";
bb869343Serge Svekolnikov8 years ago13import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
d124bf0eYuri Skorokhodov7 years ago14import { InternalErrorCode } from "../common/error/internalErrorCode";
ce5e88eeYuri Skorokhodov5 years ago15import { FileSystem } from "../common/node/fileSystem";
16import { PromiseUtil } from "../common/node/promise";
09f6024fHeniker4 years ago17import { ForkedAppWorker } from "./forkedAppWorker";
18import { ScriptImporter } from "./scriptImporter";
19
34472878RedMickey5 years ago20nls.config({
21messageFormat: nls.MessageFormat.bundle,
22bundleFormat: nls.BundleFormat.standalone,
23})();
d124bf0eYuri Skorokhodov7 years ago24const localize = nls.loadMessageBundle();
4677921cdigeff10 years ago25
e45838cbVladimir Kotikov9 years ago26export interface RNAppMessage {
ea8a5f88digeff10 years ago27method: string;
e45838cbVladimir Kotikov9 years ago28url?: string;
ea8a5f88digeff10 years ago29// These objects have also other properties but that we don't currently use
30}
5d4d4de0digeff10 years ago31
e45838cbVladimir Kotikov9 years ago32export interface IDebuggeeWorker {
ce5e88eeYuri Skorokhodov5 years ago33start(): Promise<any>;
e45838cbVladimir Kotikov9 years ago34stop(): void;
35postMessage(message: RNAppMessage): void;
36}
37
1758f9a6Yuri Skorokhodov7 years ago38function printDebuggingError(error: Error, reason: any) {
34472878RedMickey5 years ago39const nestedError = ErrorHelper.getNestedError(
40error,
41InternalErrorCode.DebuggingWontWorkReloadJSAndReconnect,
42reason,
43);
0a68f8dbArtem Egorov8 years ago44
45logger.error(nestedError.message);
039239d1Vladimir Kotikov9 years ago46}
47
34472878RedMickey5 years ago48/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
49* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
50* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
51* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
52*/
039239d1Vladimir Kotikov9 years ago53
54export class MultipleLifetimesAppWorker extends EventEmitter {
55public static WORKER_BOOTSTRAP = `
cc70057dVladimir Kotikov9 years ago56// Initialize some variables before react-native code would access them
d64a6928Vladimir Kotikov9 years ago57var onmessage=null, self=global;
cc70057dVladimir Kotikov9 years ago58// Cache Node's original require as __debug__.require
d64a6928Vladimir Kotikov9 years ago59global.__debug__={require: require};
0864d702Ruslan Bikkinin7 years ago60// Prevent leaking process.versions from debugger process to
61// worker because pure React Native doesn't do that and some packages as js-md5 rely on this behavior
62Object.defineProperty(process, "versions", {
63value: undefined
64});
7daed3fcArtem Egorov8 years ago65
4cf8fdf4Yuri Skorokhodov6 years ago66// TODO: Replace by url.fileURLToPath method when Node 10 LTS become deprecated
67function fileUrlToPath(url) {
68if (process.platform === 'win32') {
69return url.toString().replace('file:///', '');
70} else {
71return url.toString().replace('file://', '');
72}
73}
74
2ecfbd20Yuri Skorokhodov7 years ago75function getNativeModules() {
76var NativeModules;
77try {
78// This approach is for old RN versions
79NativeModules = global.require('NativeModules');
80} catch (err) {
81// ignore error and try another way for more recent RN versions
82try {
83var nativeModuleId;
84var modules = global.__r.getModules();
85var ids = Object.keys(modules);
86for (var i = 0; i < ids.length; i++) {
87if (modules[ids[i]].verboseName) {
88var packagePath = new String(modules[ids[i]].verboseName);
c3481846Yuri Skorokhodov5 years ago89if (packagePath.indexOf('Libraries/BatchedBridge/NativeModules.js') > 0 || packagePath.indexOf('Libraries\\\\BatchedBridge\\\\NativeModules.js') > 0) {
2ecfbd20Yuri Skorokhodov7 years ago90nativeModuleId = parseInt(ids[i], 10);
91break;
92}
93}
94}
95if (nativeModuleId) {
96NativeModules = global.__r(nativeModuleId);
97}
98}
99catch (err) {
100// suppress errors
101}
102}
103return NativeModules;
104}
105
106// Originally, this was made for iOS only
7daed3fcArtem Egorov8 years ago107var vscodeHandlers = {
108'vscode_reloadApp': function () {
2ecfbd20Yuri Skorokhodov7 years ago109var NativeModules = getNativeModules();
cfbe5cc9etatanova5 years ago110if (NativeModules && NativeModules.DevSettings) {
111NativeModules.DevSettings.reload();
7daed3fcArtem Egorov8 years ago112}
113},
114'vscode_showDevMenu': function () {
2ecfbd20Yuri Skorokhodov7 years ago115var NativeModules = getNativeModules();
4dfc9ffdRedMickey5 years ago116if (NativeModules && NativeModules.DevMenu) {
2ecfbd20Yuri Skorokhodov7 years ago117NativeModules.DevMenu.show();
7daed3fcArtem Egorov8 years ago118}
119}
120};
121
122process.on("message", function (message) {
123if (message.data && vscodeHandlers[message.data.method]) {
124vscodeHandlers[message.data.method]();
125} else if(onmessage) {
126onmessage(message);
127}
cc70057dVladimir Kotikov9 years ago128});
7daed3fcArtem Egorov8 years ago129
cc70057dVladimir Kotikov9 years ago130var postMessage = function(message){
131process.send(message);
132};
bb869343Serge Svekolnikov8 years ago133
134if (!self.postMessage) {
135self.postMessage = postMessage;
136}
137
cc70057dVladimir Kotikov9 years ago138var importScripts = (function(){
139var fs=require('fs'), vm=require('vm');
140return function(scriptUrl){
4cf8fdf4Yuri Skorokhodov6 years ago141scriptUrl = fileUrlToPath(scriptUrl);
142var scriptCode = fs.readFileSync(scriptUrl, 'utf8');
e67ace8aYuri Skorokhodov6 years ago143// Add a 'debugger;' statement to stop code execution
144// to wait for the sourcemaps to be processed by the debug adapter
145vm.runInThisContext('debugger;' + scriptCode, {filename: scriptUrl});
cc70057dVladimir Kotikov9 years ago146};
4cf8fdf4Yuri Skorokhodov6 years ago147})();
148`;
cc70057dVladimir Kotikov9 years ago149
cf6dd6b9Yuri Skorokhodov7 years ago150public static CONSOLE_TRACE_PATCH = `// Worker is ran as nodejs process, so console.trace() writes to stderr and it leads to error in native app
151// To avoid this console.trace() is overridden to print stacktrace via console.log()
152// Please, see Node JS implementation: https://github.com/nodejs/node/blob/master/lib/internal/console/constructor.js
153console.trace = (function() {
154return function() {
155try {
156var err = {
157name: 'Trace',
158message: require('util').format.apply(null, arguments)
159};
160// Node uses 10, but usually it's not enough for RN app trace
161Error.stackTraceLimit = 30;
162Error.captureStackTrace(err, console.trace);
163console.log(err.stack);
164} catch (e) {
165console.error(e);
166}
167};
4cf8fdf4Yuri Skorokhodov6 years ago168})();
169`;
cf6dd6b9Yuri Skorokhodov7 years ago170
13a99427Yuri Skorokhodov6 years ago171public 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:
172// https://github.com/pubkey/broadcast-channel/blob/master/src/util.js#L64
173// To avoid it if process.toString() is called if will return empty string instead of [object process].
174var nativeObjectToString = Object.prototype.toString;
175Object.prototype.toString = function() {
176if (this === process) {
177return '';
178} else {
179return nativeObjectToString.call(this);
180}
f4e3ce39Yuri Skorokhodov6 years ago181};
13a99427Yuri Skorokhodov6 years ago182`;
183
039239d1Vladimir Kotikov9 years ago184public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago185// and started listening for IPC messages
186postMessage({workerLoaded:true});`;
187
bb869343Serge Svekolnikov8 years ago188public static FETCH_STUB = `(function(self) {
4cf8fdf4Yuri Skorokhodov6 years ago189'use strict';
bb869343Serge Svekolnikov8 years ago190
4cf8fdf4Yuri Skorokhodov6 years ago191if (self.fetch) {
192return;
193}
194
195self.fetch = fetch;
bb869343Serge Svekolnikov8 years ago196
4cf8fdf4Yuri Skorokhodov6 years ago197function fetch(url) {
198return new Promise((resolve, reject) => {
199var data = require('fs').readFileSync(fileUrlToPath(url), 'utf8');
200resolve(
201{
202text: function () {
203return data;
204}
bb869343Serge Svekolnikov8 years ago205});
4cf8fdf4Yuri Skorokhodov6 years ago206});
207}
208})(global);
209`;
bb869343Serge Svekolnikov8 years ago210
6eeec3c0Serge Svekolnikov8 years ago211private packagerAddress: string;
e3ae4227digeff10 years ago212private packagerPort: number;
4677921cdigeff10 years ago213private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago214private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago215private packagerRemoteRoot?: string;
216private packagerLocalRoot?: string;
cf911877Yuri Skorokhodov7 years ago217private debuggerWorkerUrlPath?: string;
ea8a5f88digeff10 years ago218private socketToApp: WebSocket;
5940f996RedMickey5 years ago219private cancellationToken: vscode.CancellationToken;
5c8365a6Artem Egorov8 years ago220private singleLifetimeWorker: IDebuggeeWorker | null;
3b6023b2Jimmy Thomson10 years ago221private webSocketConstructor: (url: string) => WebSocket;
222
7cc67271digeff10 years ago223private executionLimiter = new ExecutionsLimiter();
ce5e88eeYuri Skorokhodov5 years ago224private nodeFileSystem = new FileSystem();
cc70057dVladimir Kotikov9 years ago225private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago226
6eeec3c0Serge Svekolnikov8 years ago227constructor(
228attachRequestArguments: any,
229sourcesStoragePath: string,
230projectRootPath: string,
5940f996RedMickey5 years ago231cancellationToken: vscode.CancellationToken,
34472878RedMickey5 years ago232{ webSocketConstructor = (url: string) => new WebSocket(url) } = {},
233) {
e45838cbVladimir Kotikov9 years ago234super();
6eeec3c0Serge Svekolnikov8 years ago235this.packagerAddress = attachRequestArguments.address || "localhost";
236this.packagerPort = attachRequestArguments.port;
237this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
238this.packagerLocalRoot = attachRequestArguments.localRoot;
cf911877Yuri Skorokhodov7 years ago239this.debuggerWorkerUrlPath = attachRequestArguments.debuggerWorkerUrlPath;
4677921cdigeff10 years ago240this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago241this.projectRootPath = projectRootPath;
5940f996RedMickey5 years ago242this.cancellationToken = cancellationToken;
1758f9a6Yuri Skorokhodov7 years ago243if (!this.sourcesStoragePath)
244throw ErrorHelper.getInternalError(InternalErrorCode.SourcesStoragePathIsNullOrEmpty);
3b6023b2Jimmy Thomson10 years ago245this.webSocketConstructor = webSocketConstructor;
34472878RedMickey5 years ago246this.scriptImporter = new ScriptImporter(
247this.packagerAddress,
248this.packagerPort,
249sourcesStoragePath,
250this.packagerRemoteRoot,
251this.packagerLocalRoot,
252);
4677921cdigeff10 years ago253}
254
0d77292aJiglioNero4 years ago255public async start(retryAttempt: boolean = false): Promise<void> {
34472878RedMickey5 years ago256const errPackagerNotRunning = ErrorHelper.getInternalError(
257InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort,
258this.packagerPort,
259);
0a68f8dbArtem Egorov8 years ago260
0d77292aJiglioNero4 years ago261await ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning);
262// Don't fetch debugger worker on socket disconnect
263if (!retryAttempt) {
264await this.downloadAndPatchDebuggerWorker();
265}
266return this.createSocketToApp(retryAttempt);
ff7dce65digeff10 years ago267}
268
34472878RedMickey5 years ago269public stop(): void {
e45838cbVladimir Kotikov9 years ago270if (this.socketToApp) {
271this.socketToApp.removeAllListeners();
272this.socketToApp.close();
273}
274
275if (this.singleLifetimeWorker) {
276this.singleLifetimeWorker.stop();
277}
278}
279
0d77292aJiglioNero4 years ago280public async downloadAndPatchDebuggerWorker(): Promise<void> {
09f6024fHeniker4 years ago281const scriptToRunPath = path.resolve(
34472878RedMickey5 years ago282this.sourcesStoragePath,
283ScriptImporter.DEBUGGER_WORKER_FILENAME,
284);
0d77292aJiglioNero4 years ago285
286await this.scriptImporter.downloadDebuggerWorker(
287this.sourcesStoragePath,
288this.projectRootPath,
289this.debuggerWorkerUrlPath,
290);
291const workerContent = await this.nodeFileSystem.readFile(scriptToRunPath, "utf8");
292const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
293// Add our customizations to debugger worker to get it working smoothly
294// in Node env and polyfill WebWorkers API over Node's IPC.
295const modifiedDebuggeeContent = [
296MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
297MultipleLifetimesAppWorker.CONSOLE_TRACE_PATCH,
298MultipleLifetimesAppWorker.PROCESS_TO_STRING_PATCH,
299isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
300workerContent,
301MultipleLifetimesAppWorker.WORKER_DONE,
302].join("\n");
303return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
cc70057dVladimir Kotikov9 years ago304}
305
7e74daf7Yuri Skorokhodov6 years ago306public showDevMenuCommand(): void {
307if (this.singleLifetimeWorker) {
308this.singleLifetimeWorker.postMessage({
309method: "vscode_showDevMenu",
310});
311}
312}
313
314public reloadAppCommand(): void {
315if (this.singleLifetimeWorker) {
316this.singleLifetimeWorker.postMessage({
317method: "vscode_reloadApp",
318});
319}
320}
321
0d77292aJiglioNero4 years ago322private async startNewWorkerLifetime(): Promise<void> {
34472878RedMickey5 years ago323this.singleLifetimeWorker = new ForkedAppWorker(
324this.packagerAddress,
325this.packagerPort,
326this.sourcesStoragePath,
327this.projectRootPath,
328message => {
6eeec3c0Serge Svekolnikov8 years ago329this.sendMessageToApp(message);
330},
34472878RedMickey5 years ago331this.packagerRemoteRoot,
332this.packagerLocalRoot,
333);
0a68f8dbArtem Egorov8 years ago334logger.verbose("A new app worker lifetime was created.");
0d77292aJiglioNero4 years ago335const startedEvent = await this.singleLifetimeWorker.start();
336this.emit("connected", startedEvent);
4677921cdigeff10 years ago337}
338
0d77292aJiglioNero4 years ago339private async createSocketToApp(retryAttempt: boolean = false): Promise<void> {
ce5e88eeYuri Skorokhodov5 years ago340return new Promise((resolve, reject) => {
341this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
342this.socketToApp.on("open", () => {
343this.onSocketOpened();
299b0557Patricio Beltran10 years ago344});
34472878RedMickey5 years ago345this.socketToApp.on("close", () => {
09f6024fHeniker4 years ago346this.executionLimiter.execute("onSocketClose.msg", /* limitInSeconds*/ 10, () => {
34472878RedMickey5 years ago347/*
348* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
349* it closes the socket because it already has a connection to a debugger.
350* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
351*/
09f6024fHeniker4 years ago352const msgKey = "_closeMessage";
34472878RedMickey5 years ago353if (this.socketToApp[msgKey] === "Another debugger is already connected") {
354reject(
355ErrorHelper.getInternalError(
356InternalErrorCode.AnotherDebuggerConnectedToPackager,
357),
358);
ce5e88eeYuri Skorokhodov5 years ago359}
34472878RedMickey5 years ago360logger.log(
361localize(
362"DisconnectedFromThePackagerToReactNative",
363"Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...",
364),
365);
ce5e88eeYuri Skorokhodov5 years ago366});
5940f996RedMickey5 years ago367if (!this.cancellationToken.isCancellationRequested) {
368setTimeout(() => {
09f6024fHeniker4 years ago369void this.start(true /* retryAttempt */);
5940f996RedMickey5 years ago370}, 100);
371}
34472878RedMickey5 years ago372});
373this.socketToApp.on("message", (message: any) => this.onMessage(message));
374this.socketToApp.on("error", (error: Error) => {
375if (retryAttempt) {
376printDebuggingError(
377ErrorHelper.getInternalError(
378InternalErrorCode.ReconnectionToPackagerFailedCheckForErrorsOrRestartReactNative,
379),
380error,
381);
382}
383
384reject(error);
385});
32cab018Meena Kunnathur Balakrishnan10 years ago386
ce5e88eeYuri Skorokhodov5 years ago387// In an attempt to catch failures in starting the packager on first attempt,
388// wait for 300 ms before resolving the promise
09f6024fHeniker4 years ago389void PromiseUtil.delay(300).then(() => resolve());
ce5e88eeYuri Skorokhodov5 years ago390});
4677921cdigeff10 years ago391}
392
393private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago394return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago395}
396
ea8a5f88digeff10 years ago397private onSocketOpened() {
09f6024fHeniker4 years ago398this.executionLimiter.execute("onSocketOpened.msg", /* limitInSeconds*/ 10, () =>
34472878RedMickey5 years ago399logger.log(
400localize(
401"EstablishedConnectionWithPackagerToReactNativeApp",
402"Established a connection with the Proxy (Packager) to the React Native application",
403),
404),
405);
4677921cdigeff10 years ago406}
407
9174feb7Vladimir Kotikov9 years ago408private killWorker() {
409if (!this.singleLifetimeWorker) return;
410this.singleLifetimeWorker.stop();
411this.singleLifetimeWorker = null;
412}
e7b314e8Vladimir Kotikov9 years ago413
9174feb7Vladimir Kotikov9 years ago414private onMessage(message: string) {
5d4d4de0digeff10 years ago415try {
09f6024fHeniker4 years ago416logger.verbose(`From RN APP: ${message}`);
417const object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago418if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago419// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
420// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago421this.killWorker();
5d4d4de0digeff10 years ago422// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
423this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago424} else if (object.method === "$disconnected") {
425// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago426this.killWorker();
5d4d4de0digeff10 years ago427} else if (object.method) {
428// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago429if (this.singleLifetimeWorker) {
430this.singleLifetimeWorker.postMessage(object);
431}
5d4d4de0digeff10 years ago432} else {
ea8a5f88digeff10 years ago433// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
34472878RedMickey5 years ago434logger.verbose(
435`The react-native app sent a message without specifying a method: ${message}`,
436);
5d4d4de0digeff10 years ago437}
438} catch (exception) {
34472878RedMickey5 years ago439printDebuggingError(
440ErrorHelper.getInternalError(
441InternalErrorCode.FailedToProcessMessageFromReactNativeApp,
442message,
443),
444exception,
445);
4677921cdigeff10 years ago446}
447}
448
449private gotPrepareJSRuntime(message: any): void {
450// Create the sandbox, and replay that we finished processing the message
34472878RedMickey5 years ago451this.startNewWorkerLifetime().then(
452() => {
453this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
454},
455error =>
456printDebuggingError(
457ErrorHelper.getInternalError(
458InternalErrorCode.FailedToPrepareJSRuntimeEnvironment,
459message,
460),
461error,
462),
463);
4677921cdigeff10 years ago464}
465
ff7dce65digeff10 years ago466private sendMessageToApp(message: any): void {
09f6024fHeniker4 years ago467let stringified = "";
354c28a1digeff10 years ago468try {
469stringified = JSON.stringify(message);
d124bf0eYuri Skorokhodov7 years ago470logger.verbose(`To RN APP: ${stringified}`);
354c28a1digeff10 years ago471this.socketToApp.send(stringified);
472} catch (exception) {
09f6024fHeniker4 years ago473const messageToShow = stringified || String(message); // Try to show the stringified version, but show the toString if unavailable
34472878RedMickey5 years ago474printDebuggingError(
475ErrorHelper.getInternalError(
476InternalErrorCode.FailedToSendMessageToTheReactNativeApp,
477messageToShow,
478),
479exception,
480);
354c28a1digeff10 years ago481}
4677921cdigeff10 years ago482}
483}