microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
indexed-sourcemap-null-section-issue

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

487lines · 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";
176f99c8ConnorQi013 months ago7import WebSocket = require("ws");
623be8a6Ezio Li2 years ago8import { logger } from "@vscode/debugadapter";
09f6024fHeniker4 years ago9import * 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
4c6a26c0ConnorQi012 months ago66// Polyfill for url.fileURLToPath - needed because this code runs in user's RN environment
67// where we cannot use import/require. Keep this implementation.
4cf8fdf4Yuri Skorokhodov6 years ago68function fileUrlToPath(url) {
69if (process.platform === 'win32') {
70return url.toString().replace('file:///', '');
71} else {
72return url.toString().replace('file://', '');
73}
74}
75
2ecfbd20Yuri Skorokhodov7 years ago76function getNativeModules() {
77var NativeModules;
78try {
79// This approach is for old RN versions
80NativeModules = global.require('NativeModules');
81} catch (err) {
82// ignore error and try another way for more recent RN versions
83try {
84var nativeModuleId;
85var modules = global.__r.getModules();
86var ids = Object.keys(modules);
87for (var i = 0; i < ids.length; i++) {
88if (modules[ids[i]].verboseName) {
89var packagePath = new String(modules[ids[i]].verboseName);
c3481846Yuri Skorokhodov5 years ago90if (packagePath.indexOf('Libraries/BatchedBridge/NativeModules.js') > 0 || packagePath.indexOf('Libraries\\\\BatchedBridge\\\\NativeModules.js') > 0) {
2ecfbd20Yuri Skorokhodov7 years ago91nativeModuleId = parseInt(ids[i], 10);
92break;
93}
94}
95}
96if (nativeModuleId) {
97NativeModules = global.__r(nativeModuleId);
98}
99}
100catch (err) {
101// suppress errors
102}
103}
104return NativeModules;
105}
106
107// Originally, this was made for iOS only
7daed3fcArtem Egorov8 years ago108var vscodeHandlers = {
109'vscode_reloadApp': function () {
2ecfbd20Yuri Skorokhodov7 years ago110var NativeModules = getNativeModules();
cfbe5cc9etatanova5 years ago111if (NativeModules && NativeModules.DevSettings) {
112NativeModules.DevSettings.reload();
7daed3fcArtem Egorov8 years ago113}
114},
115'vscode_showDevMenu': function () {
2ecfbd20Yuri Skorokhodov7 years ago116var NativeModules = getNativeModules();
4dfc9ffdRedMickey5 years ago117if (NativeModules && NativeModules.DevMenu) {
2ecfbd20Yuri Skorokhodov7 years ago118NativeModules.DevMenu.show();
7daed3fcArtem Egorov8 years ago119}
120}
121};
122
123process.on("message", function (message) {
124if (message.data && vscodeHandlers[message.data.method]) {
125vscodeHandlers[message.data.method]();
126} else if(onmessage) {
127onmessage(message);
128}
cc70057dVladimir Kotikov9 years ago129});
7daed3fcArtem Egorov8 years ago130
cc70057dVladimir Kotikov9 years ago131var postMessage = function(message){
132process.send(message);
133};
bb869343Serge Svekolnikov8 years ago134
135if (!self.postMessage) {
136self.postMessage = postMessage;
137}
138
cc70057dVladimir Kotikov9 years ago139var importScripts = (function(){
140var fs=require('fs'), vm=require('vm');
141return function(scriptUrl){
4cf8fdf4Yuri Skorokhodov6 years ago142scriptUrl = fileUrlToPath(scriptUrl);
143var scriptCode = fs.readFileSync(scriptUrl, 'utf8');
e67ace8aYuri Skorokhodov6 years ago144// Add a 'debugger;' statement to stop code execution
145// to wait for the sourcemaps to be processed by the debug adapter
146vm.runInThisContext('debugger;' + scriptCode, {filename: scriptUrl});
cc70057dVladimir Kotikov9 years ago147};
4cf8fdf4Yuri Skorokhodov6 years ago148})();
149`;
cc70057dVladimir Kotikov9 years ago150
cf6dd6b9Yuri Skorokhodov7 years ago151public static CONSOLE_TRACE_PATCH = `// Worker is ran as nodejs process, so console.trace() writes to stderr and it leads to error in native app
152// To avoid this console.trace() is overridden to print stacktrace via console.log()
153// Please, see Node JS implementation: https://github.com/nodejs/node/blob/master/lib/internal/console/constructor.js
154console.trace = (function() {
155return function() {
156try {
157var err = {
158name: 'Trace',
159message: require('util').format.apply(null, arguments)
160};
161// Node uses 10, but usually it's not enough for RN app trace
162Error.stackTraceLimit = 30;
163Error.captureStackTrace(err, console.trace);
164console.log(err.stack);
165} catch (e) {
166console.error(e);
167}
168};
4cf8fdf4Yuri Skorokhodov6 years ago169})();
170`;
cf6dd6b9Yuri Skorokhodov7 years ago171
13a99427Yuri Skorokhodov6 years ago172public 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:
173// https://github.com/pubkey/broadcast-channel/blob/master/src/util.js#L64
174// To avoid it if process.toString() is called if will return empty string instead of [object process].
175var nativeObjectToString = Object.prototype.toString;
176Object.prototype.toString = function() {
177if (this === process) {
178return '';
179} else {
180return nativeObjectToString.call(this);
181}
f4e3ce39Yuri Skorokhodov6 years ago182};
13a99427Yuri Skorokhodov6 years ago183`;
184
039239d1Vladimir Kotikov9 years ago185public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago186// and started listening for IPC messages
187postMessage({workerLoaded:true});`;
188
bb869343Serge Svekolnikov8 years ago189public static FETCH_STUB = `(function(self) {
4cf8fdf4Yuri Skorokhodov6 years ago190'use strict';
bb869343Serge Svekolnikov8 years ago191
4cf8fdf4Yuri Skorokhodov6 years ago192if (self.fetch) {
193return;
194}
195
196self.fetch = fetch;
bb869343Serge Svekolnikov8 years ago197
4cf8fdf4Yuri Skorokhodov6 years ago198function fetch(url) {
199return new Promise((resolve, reject) => {
200var data = require('fs').readFileSync(fileUrlToPath(url), 'utf8');
201resolve(
202{
203text: function () {
204return data;
205}
bb869343Serge Svekolnikov8 years ago206});
4cf8fdf4Yuri Skorokhodov6 years ago207});
208}
209})(global);
210`;
bb869343Serge Svekolnikov8 years ago211
6eeec3c0Serge Svekolnikov8 years ago212private packagerAddress: string;
e3ae4227digeff10 years ago213private packagerPort: number;
4677921cdigeff10 years ago214private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago215private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago216private packagerRemoteRoot?: string;
217private packagerLocalRoot?: string;
cf911877Yuri Skorokhodov7 years ago218private debuggerWorkerUrlPath?: string;
176f99c8ConnorQi013 months ago219private socketToApp!: WebSocket;
5940f996RedMickey5 years ago220private cancellationToken: vscode.CancellationToken;
176f99c8ConnorQi013 months ago221private singleLifetimeWorker: IDebuggeeWorker | null = null;
3b6023b2Jimmy Thomson10 years ago222private webSocketConstructor: (url: string) => WebSocket;
223
7cc67271digeff10 years ago224private executionLimiter = new ExecutionsLimiter();
ce5e88eeYuri Skorokhodov5 years ago225private nodeFileSystem = new FileSystem();
cc70057dVladimir Kotikov9 years ago226private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago227
6eeec3c0Serge Svekolnikov8 years ago228constructor(
229attachRequestArguments: any,
230sourcesStoragePath: string,
231projectRootPath: string,
5940f996RedMickey5 years ago232cancellationToken: vscode.CancellationToken,
34472878RedMickey5 years ago233{ webSocketConstructor = (url: string) => new WebSocket(url) } = {},
234) {
e45838cbVladimir Kotikov9 years ago235super();
6eeec3c0Serge Svekolnikov8 years ago236this.packagerAddress = attachRequestArguments.address || "localhost";
237this.packagerPort = attachRequestArguments.port;
238this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
239this.packagerLocalRoot = attachRequestArguments.localRoot;
cf911877Yuri Skorokhodov7 years ago240this.debuggerWorkerUrlPath = attachRequestArguments.debuggerWorkerUrlPath;
4677921cdigeff10 years ago241this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago242this.projectRootPath = projectRootPath;
5940f996RedMickey5 years ago243this.cancellationToken = cancellationToken;
1758f9a6Yuri Skorokhodov7 years ago244if (!this.sourcesStoragePath)
245throw ErrorHelper.getInternalError(InternalErrorCode.SourcesStoragePathIsNullOrEmpty);
3b6023b2Jimmy Thomson10 years ago246this.webSocketConstructor = webSocketConstructor;
34472878RedMickey5 years ago247this.scriptImporter = new ScriptImporter(
248this.packagerAddress,
249this.packagerPort,
250sourcesStoragePath,
251this.packagerRemoteRoot,
252this.packagerLocalRoot,
253);
4677921cdigeff10 years ago254}
255
0d77292aJiglioNero4 years ago256public async start(retryAttempt: boolean = false): Promise<void> {
34472878RedMickey5 years ago257const errPackagerNotRunning = ErrorHelper.getInternalError(
258InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort,
259this.packagerPort,
260);
0a68f8dbArtem Egorov8 years ago261
0d77292aJiglioNero4 years ago262await ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning);
263// Don't fetch debugger worker on socket disconnect
264if (!retryAttempt) {
265await this.downloadAndPatchDebuggerWorker();
266}
267return this.createSocketToApp(retryAttempt);
ff7dce65digeff10 years ago268}
269
34472878RedMickey5 years ago270public stop(): void {
e45838cbVladimir Kotikov9 years ago271if (this.socketToApp) {
272this.socketToApp.removeAllListeners();
273this.socketToApp.close();
274}
275
276if (this.singleLifetimeWorker) {
277this.singleLifetimeWorker.stop();
278}
279}
280
0d77292aJiglioNero4 years ago281public async downloadAndPatchDebuggerWorker(): Promise<void> {
09f6024fHeniker4 years ago282const scriptToRunPath = path.resolve(
34472878RedMickey5 years ago283this.sourcesStoragePath,
284ScriptImporter.DEBUGGER_WORKER_FILENAME,
285);
0d77292aJiglioNero4 years ago286
287await this.scriptImporter.downloadDebuggerWorker(
288this.sourcesStoragePath,
289this.projectRootPath,
290this.debuggerWorkerUrlPath,
291);
292const workerContent = await this.nodeFileSystem.readFile(scriptToRunPath, "utf8");
293const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
294// Add our customizations to debugger worker to get it working smoothly
295// in Node env and polyfill WebWorkers API over Node's IPC.
296const modifiedDebuggeeContent = [
297MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
298MultipleLifetimesAppWorker.CONSOLE_TRACE_PATCH,
299MultipleLifetimesAppWorker.PROCESS_TO_STRING_PATCH,
300isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
301workerContent,
302MultipleLifetimesAppWorker.WORKER_DONE,
303].join("\n");
304return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
cc70057dVladimir Kotikov9 years ago305}
306
7e74daf7Yuri Skorokhodov6 years ago307public showDevMenuCommand(): void {
308if (this.singleLifetimeWorker) {
309this.singleLifetimeWorker.postMessage({
310method: "vscode_showDevMenu",
311});
312}
313}
314
315public reloadAppCommand(): void {
316if (this.singleLifetimeWorker) {
317this.singleLifetimeWorker.postMessage({
318method: "vscode_reloadApp",
319});
320}
321}
322
0d77292aJiglioNero4 years ago323private async startNewWorkerLifetime(): Promise<void> {
34472878RedMickey5 years ago324this.singleLifetimeWorker = new ForkedAppWorker(
325this.packagerAddress,
326this.packagerPort,
327this.sourcesStoragePath,
328this.projectRootPath,
329message => {
6eeec3c0Serge Svekolnikov8 years ago330this.sendMessageToApp(message);
331},
34472878RedMickey5 years ago332this.packagerRemoteRoot,
333this.packagerLocalRoot,
334);
0a68f8dbArtem Egorov8 years ago335logger.verbose("A new app worker lifetime was created.");
0d77292aJiglioNero4 years ago336const startedEvent = await this.singleLifetimeWorker.start();
337this.emit("connected", startedEvent);
4677921cdigeff10 years ago338}
339
0d77292aJiglioNero4 years ago340private async createSocketToApp(retryAttempt: boolean = false): Promise<void> {
ce5e88eeYuri Skorokhodov5 years ago341return new Promise((resolve, reject) => {
342this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
343this.socketToApp.on("open", () => {
344this.onSocketOpened();
299b0557Patricio Beltran10 years ago345});
34472878RedMickey5 years ago346this.socketToApp.on("close", () => {
09f6024fHeniker4 years ago347this.executionLimiter.execute("onSocketClose.msg", /* limitInSeconds*/ 10, () => {
34472878RedMickey5 years ago348/*
349* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
350* it closes the socket because it already has a connection to a debugger.
351* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
352*/
09f6024fHeniker4 years ago353const msgKey = "_closeMessage";
5b09cf97Zhen Zhen Yuan (BEYONDSOFT CONSULTING INC)7 months ago354if (
355(this.socketToApp as any)[msgKey] ===
356"Another debugger is already connected"
357) {
34472878RedMickey5 years ago358reject(
359ErrorHelper.getInternalError(
360InternalErrorCode.AnotherDebuggerConnectedToPackager,
361),
362);
ce5e88eeYuri Skorokhodov5 years ago363}
34472878RedMickey5 years ago364logger.log(
365localize(
366"DisconnectedFromThePackagerToReactNative",
367"Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...",
368),
369);
ce5e88eeYuri Skorokhodov5 years ago370});
5940f996RedMickey5 years ago371if (!this.cancellationToken.isCancellationRequested) {
372setTimeout(() => {
09f6024fHeniker4 years ago373void this.start(true /* retryAttempt */);
5940f996RedMickey5 years ago374}, 100);
375}
34472878RedMickey5 years ago376});
377this.socketToApp.on("message", (message: any) => this.onMessage(message));
378this.socketToApp.on("error", (error: Error) => {
379if (retryAttempt) {
380printDebuggingError(
381ErrorHelper.getInternalError(
382InternalErrorCode.ReconnectionToPackagerFailedCheckForErrorsOrRestartReactNative,
383),
384error,
385);
386}
387
388reject(error);
389});
32cab018Meena Kunnathur Balakrishnan10 years ago390
ce5e88eeYuri Skorokhodov5 years ago391// In an attempt to catch failures in starting the packager on first attempt,
392// wait for 300 ms before resolving the promise
09f6024fHeniker4 years ago393void PromiseUtil.delay(300).then(() => resolve());
ce5e88eeYuri Skorokhodov5 years ago394});
4677921cdigeff10 years ago395}
396
397private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago398return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago399}
400
ea8a5f88digeff10 years ago401private onSocketOpened() {
09f6024fHeniker4 years ago402this.executionLimiter.execute("onSocketOpened.msg", /* limitInSeconds*/ 10, () =>
34472878RedMickey5 years ago403logger.log(
404localize(
405"EstablishedConnectionWithPackagerToReactNativeApp",
406"Established a connection with the Proxy (Packager) to the React Native application",
407),
408),
409);
4677921cdigeff10 years ago410}
411
9174feb7Vladimir Kotikov9 years ago412private killWorker() {
413if (!this.singleLifetimeWorker) return;
414this.singleLifetimeWorker.stop();
415this.singleLifetimeWorker = null;
416}
e7b314e8Vladimir Kotikov9 years ago417
9174feb7Vladimir Kotikov9 years ago418private onMessage(message: string) {
5d4d4de0digeff10 years ago419try {
09f6024fHeniker4 years ago420logger.verbose(`From RN APP: ${message}`);
421const object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago422if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago423// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
424// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago425this.killWorker();
5d4d4de0digeff10 years ago426// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
427this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago428} else if (object.method === "$disconnected") {
429// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago430this.killWorker();
5d4d4de0digeff10 years ago431} else if (object.method) {
432// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago433if (this.singleLifetimeWorker) {
434this.singleLifetimeWorker.postMessage(object);
435}
5d4d4de0digeff10 years ago436} else {
ea8a5f88digeff10 years ago437// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
34472878RedMickey5 years ago438logger.verbose(
439`The react-native app sent a message without specifying a method: ${message}`,
440);
5d4d4de0digeff10 years ago441}
442} catch (exception) {
34472878RedMickey5 years ago443printDebuggingError(
444ErrorHelper.getInternalError(
445InternalErrorCode.FailedToProcessMessageFromReactNativeApp,
446message,
447),
448exception,
449);
4677921cdigeff10 years ago450}
451}
452
453private gotPrepareJSRuntime(message: any): void {
454// Create the sandbox, and replay that we finished processing the message
34472878RedMickey5 years ago455this.startNewWorkerLifetime().then(
456() => {
457this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
458},
459error =>
460printDebuggingError(
461ErrorHelper.getInternalError(
462InternalErrorCode.FailedToPrepareJSRuntimeEnvironment,
463message,
464),
465error,
466),
467);
4677921cdigeff10 years ago468}
469
ff7dce65digeff10 years ago470private sendMessageToApp(message: any): void {
09f6024fHeniker4 years ago471let stringified = "";
354c28a1digeff10 years ago472try {
473stringified = JSON.stringify(message);
d124bf0eYuri Skorokhodov7 years ago474logger.verbose(`To RN APP: ${stringified}`);
354c28a1digeff10 years ago475this.socketToApp.send(stringified);
476} catch (exception) {
09f6024fHeniker4 years ago477const messageToShow = stringified || String(message); // Try to show the stringified version, but show the toString if unavailable
34472878RedMickey5 years ago478printDebuggingError(
479ErrorHelper.getInternalError(
480InternalErrorCode.FailedToSendMessageToTheReactNativeApp,
481messageToShow,
482),
483exception,
484);
354c28a1digeff10 years ago485}
4677921cdigeff10 years ago486}
487}