microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.3.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

479lines · 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";
34472878RedMickey5 years ago8import { ErrorHelper } from "../common/error/errorHelper";
a6562589RedMickey6 years ago9import { logger } from "vscode-debugadapter";
34472878RedMickey5 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";
34472878RedMickey5 years ago18nls.config({
19messageFormat: nls.MessageFormat.bundle,
20bundleFormat: nls.BundleFormat.standalone,
21})();
d124bf0eYuri Skorokhodov7 years ago22const localize = nls.loadMessageBundle();
4677921cdigeff10 years ago23
e45838cbVladimir Kotikov9 years ago24export interface RNAppMessage {
ea8a5f88digeff10 years ago25method: string;
e45838cbVladimir Kotikov9 years ago26url?: string;
ea8a5f88digeff10 years ago27// These objects have also other properties but that we don't currently use
28}
5d4d4de0digeff10 years ago29
e45838cbVladimir Kotikov9 years ago30export interface IDebuggeeWorker {
ce5e88eeYuri Skorokhodov5 years ago31start(): Promise<any>;
e45838cbVladimir Kotikov9 years ago32stop(): void;
33postMessage(message: RNAppMessage): void;
34}
35
1758f9a6Yuri Skorokhodov7 years ago36function printDebuggingError(error: Error, reason: any) {
34472878RedMickey5 years ago37const nestedError = ErrorHelper.getNestedError(
38error,
39InternalErrorCode.DebuggingWontWorkReloadJSAndReconnect,
40reason,
41);
0a68f8dbArtem Egorov8 years ago42
43logger.error(nestedError.message);
039239d1Vladimir Kotikov9 years ago44}
45
34472878RedMickey5 years ago46/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
47* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
48* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
49* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
50*/
039239d1Vladimir Kotikov9 years ago51
52export class MultipleLifetimesAppWorker extends EventEmitter {
53public static WORKER_BOOTSTRAP = `
cc70057dVladimir Kotikov9 years ago54// Initialize some variables before react-native code would access them
d64a6928Vladimir Kotikov9 years ago55var onmessage=null, self=global;
cc70057dVladimir Kotikov9 years ago56// Cache Node's original require as __debug__.require
d64a6928Vladimir Kotikov9 years ago57global.__debug__={require: require};
0864d702Ruslan Bikkinin7 years ago58// Prevent leaking process.versions from debugger process to
59// worker because pure React Native doesn't do that and some packages as js-md5 rely on this behavior
60Object.defineProperty(process, "versions", {
61value: undefined
62});
7daed3fcArtem Egorov8 years ago63
4cf8fdf4Yuri Skorokhodov6 years ago64// TODO: Replace by url.fileURLToPath method when Node 10 LTS become deprecated
65function fileUrlToPath(url) {
66if (process.platform === 'win32') {
67return url.toString().replace('file:///', '');
68} else {
69return url.toString().replace('file://', '');
70}
71}
72
2ecfbd20Yuri Skorokhodov7 years ago73function getNativeModules() {
74var NativeModules;
75try {
76// This approach is for old RN versions
77NativeModules = global.require('NativeModules');
78} catch (err) {
79// ignore error and try another way for more recent RN versions
80try {
81var nativeModuleId;
82var modules = global.__r.getModules();
83var ids = Object.keys(modules);
84for (var i = 0; i < ids.length; i++) {
85if (modules[ids[i]].verboseName) {
86var packagePath = new String(modules[ids[i]].verboseName);
c3481846Yuri Skorokhodov5 years ago87if (packagePath.indexOf('Libraries/BatchedBridge/NativeModules.js') > 0 || packagePath.indexOf('Libraries\\\\BatchedBridge\\\\NativeModules.js') > 0) {
2ecfbd20Yuri Skorokhodov7 years ago88nativeModuleId = parseInt(ids[i], 10);
89break;
90}
91}
92}
93if (nativeModuleId) {
94NativeModules = global.__r(nativeModuleId);
95}
96}
97catch (err) {
98// suppress errors
99}
100}
101return NativeModules;
102}
103
104// Originally, this was made for iOS only
7daed3fcArtem Egorov8 years ago105var vscodeHandlers = {
106'vscode_reloadApp': function () {
2ecfbd20Yuri Skorokhodov7 years ago107var NativeModules = getNativeModules();
4dfc9ffdRedMickey5 years ago108if (NativeModules && NativeModules.DevMenu) {
2ecfbd20Yuri Skorokhodov7 years ago109NativeModules.DevMenu.reload();
7daed3fcArtem Egorov8 years ago110}
111},
112'vscode_showDevMenu': function () {
2ecfbd20Yuri Skorokhodov7 years ago113var NativeModules = getNativeModules();
4dfc9ffdRedMickey5 years ago114if (NativeModules && NativeModules.DevMenu) {
2ecfbd20Yuri Skorokhodov7 years ago115NativeModules.DevMenu.show();
7daed3fcArtem Egorov8 years ago116}
117}
118};
119
120process.on("message", function (message) {
121if (message.data && vscodeHandlers[message.data.method]) {
122vscodeHandlers[message.data.method]();
123} else if(onmessage) {
124onmessage(message);
125}
cc70057dVladimir Kotikov9 years ago126});
7daed3fcArtem Egorov8 years ago127
cc70057dVladimir Kotikov9 years ago128var postMessage = function(message){
129process.send(message);
130};
bb869343Serge Svekolnikov8 years ago131
132if (!self.postMessage) {
133self.postMessage = postMessage;
134}
135
cc70057dVladimir Kotikov9 years ago136var importScripts = (function(){
137var fs=require('fs'), vm=require('vm');
138return function(scriptUrl){
4cf8fdf4Yuri Skorokhodov6 years ago139scriptUrl = fileUrlToPath(scriptUrl);
140var scriptCode = fs.readFileSync(scriptUrl, 'utf8');
e67ace8aYuri Skorokhodov6 years ago141// Add a 'debugger;' statement to stop code execution
142// to wait for the sourcemaps to be processed by the debug adapter
143vm.runInThisContext('debugger;' + scriptCode, {filename: scriptUrl});
cc70057dVladimir Kotikov9 years ago144};
4cf8fdf4Yuri Skorokhodov6 years ago145})();
146`;
cc70057dVladimir Kotikov9 years ago147
cf6dd6b9Yuri Skorokhodov7 years ago148public static CONSOLE_TRACE_PATCH = `// Worker is ran as nodejs process, so console.trace() writes to stderr and it leads to error in native app
149// To avoid this console.trace() is overridden to print stacktrace via console.log()
150// Please, see Node JS implementation: https://github.com/nodejs/node/blob/master/lib/internal/console/constructor.js
151console.trace = (function() {
152return function() {
153try {
154var err = {
155name: 'Trace',
156message: require('util').format.apply(null, arguments)
157};
158// Node uses 10, but usually it's not enough for RN app trace
159Error.stackTraceLimit = 30;
160Error.captureStackTrace(err, console.trace);
161console.log(err.stack);
162} catch (e) {
163console.error(e);
164}
165};
4cf8fdf4Yuri Skorokhodov6 years ago166})();
167`;
cf6dd6b9Yuri Skorokhodov7 years ago168
13a99427Yuri Skorokhodov6 years ago169public 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:
170// https://github.com/pubkey/broadcast-channel/blob/master/src/util.js#L64
171// To avoid it if process.toString() is called if will return empty string instead of [object process].
172var nativeObjectToString = Object.prototype.toString;
173Object.prototype.toString = function() {
174if (this === process) {
175return '';
176} else {
177return nativeObjectToString.call(this);
178}
f4e3ce39Yuri Skorokhodov6 years ago179};
13a99427Yuri Skorokhodov6 years ago180`;
181
039239d1Vladimir Kotikov9 years ago182public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago183// and started listening for IPC messages
184postMessage({workerLoaded:true});`;
185
bb869343Serge Svekolnikov8 years ago186public static FETCH_STUB = `(function(self) {
4cf8fdf4Yuri Skorokhodov6 years ago187'use strict';
bb869343Serge Svekolnikov8 years ago188
4cf8fdf4Yuri Skorokhodov6 years ago189if (self.fetch) {
190return;
191}
192
193self.fetch = fetch;
bb869343Serge Svekolnikov8 years ago194
4cf8fdf4Yuri Skorokhodov6 years ago195function fetch(url) {
196return new Promise((resolve, reject) => {
197var data = require('fs').readFileSync(fileUrlToPath(url), 'utf8');
198resolve(
199{
200text: function () {
201return data;
202}
bb869343Serge Svekolnikov8 years ago203});
4cf8fdf4Yuri Skorokhodov6 years ago204});
205}
206})(global);
207`;
bb869343Serge Svekolnikov8 years ago208
6eeec3c0Serge Svekolnikov8 years ago209private packagerAddress: string;
e3ae4227digeff10 years ago210private packagerPort: number;
4677921cdigeff10 years ago211private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago212private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago213private packagerRemoteRoot?: string;
214private packagerLocalRoot?: string;
cf911877Yuri Skorokhodov7 years ago215private debuggerWorkerUrlPath?: string;
ea8a5f88digeff10 years ago216private socketToApp: WebSocket;
5c8365a6Artem Egorov8 years ago217private singleLifetimeWorker: IDebuggeeWorker | null;
3b6023b2Jimmy Thomson10 years ago218private webSocketConstructor: (url: string) => WebSocket;
219
7cc67271digeff10 years ago220private executionLimiter = new ExecutionsLimiter();
ce5e88eeYuri Skorokhodov5 years ago221private nodeFileSystem = new FileSystem();
cc70057dVladimir Kotikov9 years ago222private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago223
6eeec3c0Serge Svekolnikov8 years ago224constructor(
225attachRequestArguments: any,
226sourcesStoragePath: string,
227projectRootPath: string,
34472878RedMickey5 years ago228{ webSocketConstructor = (url: string) => new WebSocket(url) } = {},
229) {
e45838cbVladimir Kotikov9 years ago230super();
6eeec3c0Serge Svekolnikov8 years ago231this.packagerAddress = attachRequestArguments.address || "localhost";
232this.packagerPort = attachRequestArguments.port;
233this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
234this.packagerLocalRoot = attachRequestArguments.localRoot;
cf911877Yuri Skorokhodov7 years ago235this.debuggerWorkerUrlPath = attachRequestArguments.debuggerWorkerUrlPath;
4677921cdigeff10 years ago236this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago237this.projectRootPath = projectRootPath;
1758f9a6Yuri Skorokhodov7 years ago238if (!this.sourcesStoragePath)
239throw ErrorHelper.getInternalError(InternalErrorCode.SourcesStoragePathIsNullOrEmpty);
3b6023b2Jimmy Thomson10 years ago240this.webSocketConstructor = webSocketConstructor;
34472878RedMickey5 years ago241this.scriptImporter = new ScriptImporter(
242this.packagerAddress,
243this.packagerPort,
244sourcesStoragePath,
245this.packagerRemoteRoot,
246this.packagerLocalRoot,
247);
4677921cdigeff10 years ago248}
249
ce5e88eeYuri Skorokhodov5 years ago250public start(retryAttempt: boolean = false): Promise<any> {
34472878RedMickey5 years ago251const errPackagerNotRunning = ErrorHelper.getInternalError(
252InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort,
253this.packagerPort,
254);
0a68f8dbArtem Egorov8 years ago255
6eeec3c0Serge Svekolnikov8 years ago256return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
cc70057dVladimir Kotikov9 years ago257.then(() => {
258// Don't fetch debugger worker on socket disconnect
34472878RedMickey5 years ago259return retryAttempt ? Promise.resolve() : this.downloadAndPatchDebuggerWorker();
cc70057dVladimir Kotikov9 years ago260})
261.then(() => this.createSocketToApp(retryAttempt));
ff7dce65digeff10 years ago262}
263
34472878RedMickey5 years ago264public stop(): void {
e45838cbVladimir Kotikov9 years ago265if (this.socketToApp) {
266this.socketToApp.removeAllListeners();
267this.socketToApp.close();
268}
269
270if (this.singleLifetimeWorker) {
271this.singleLifetimeWorker.stop();
272}
273}
274
ce5e88eeYuri Skorokhodov5 years ago275public downloadAndPatchDebuggerWorker(): Promise<void> {
34472878RedMickey5 years ago276let scriptToRunPath = path.resolve(
277this.sourcesStoragePath,
278ScriptImporter.DEBUGGER_WORKER_FILENAME,
279);
280return this.scriptImporter
281.downloadDebuggerWorker(
282this.sourcesStoragePath,
283this.projectRootPath,
284this.debuggerWorkerUrlPath,
285)
cc70057dVladimir Kotikov9 years ago286.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
287.then((workerContent: string) => {
bb869343Serge Svekolnikov8 years ago288const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
cc70057dVladimir Kotikov9 years ago289// Add our customizations to debugger worker to get it working smoothly
290// in Node env and polyfill WebWorkers API over Node's IPC.
bb869343Serge Svekolnikov8 years ago291const modifiedDebuggeeContent = [
292MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
cf6dd6b9Yuri Skorokhodov7 years ago293MultipleLifetimesAppWorker.CONSOLE_TRACE_PATCH,
13a99427Yuri Skorokhodov6 years ago294MultipleLifetimesAppWorker.PROCESS_TO_STRING_PATCH,
bb869343Serge Svekolnikov8 years ago295isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
296workerContent,
297MultipleLifetimesAppWorker.WORKER_DONE,
298].join("\n");
cc70057dVladimir Kotikov9 years ago299return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
300});
301}
302
7e74daf7Yuri Skorokhodov6 years ago303public showDevMenuCommand(): void {
304if (this.singleLifetimeWorker) {
305this.singleLifetimeWorker.postMessage({
306method: "vscode_showDevMenu",
307});
308}
309}
310
311public reloadAppCommand(): void {
312if (this.singleLifetimeWorker) {
313this.singleLifetimeWorker.postMessage({
314method: "vscode_reloadApp",
315});
316}
317}
318
ce5e88eeYuri Skorokhodov5 years ago319private startNewWorkerLifetime(): Promise<void> {
34472878RedMickey5 years ago320this.singleLifetimeWorker = new ForkedAppWorker(
321this.packagerAddress,
322this.packagerPort,
323this.sourcesStoragePath,
324this.projectRootPath,
325message => {
6eeec3c0Serge Svekolnikov8 years ago326this.sendMessageToApp(message);
327},
34472878RedMickey5 years ago328this.packagerRemoteRoot,
329this.packagerLocalRoot,
330);
0a68f8dbArtem Egorov8 years ago331logger.verbose("A new app worker lifetime was created.");
34472878RedMickey5 years ago332return this.singleLifetimeWorker.start().then(startedEvent => {
333this.emit("connected", startedEvent);
334});
4677921cdigeff10 years ago335}
336
ce5e88eeYuri Skorokhodov5 years ago337private createSocketToApp(retryAttempt: boolean = false): Promise<void> {
338return new Promise((resolve, reject) => {
339this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
340this.socketToApp.on("open", () => {
341this.onSocketOpened();
299b0557Patricio Beltran10 years ago342});
34472878RedMickey5 years ago343this.socketToApp.on("close", () => {
344this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
345/*
346* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
347* it closes the socket because it already has a connection to a debugger.
348* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
349*/
350let msgKey = "_closeMessage";
351if (this.socketToApp[msgKey] === "Another debugger is already connected") {
352reject(
353ErrorHelper.getInternalError(
354InternalErrorCode.AnotherDebuggerConnectedToPackager,
355),
356);
ce5e88eeYuri Skorokhodov5 years ago357}
34472878RedMickey5 years ago358logger.log(
359localize(
360"DisconnectedFromThePackagerToReactNative",
361"Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...",
362),
363);
ce5e88eeYuri Skorokhodov5 years ago364});
34472878RedMickey5 years ago365setTimeout(() => {
366this.start(true /* retryAttempt */);
367}, 100);
368});
369this.socketToApp.on("message", (message: any) => this.onMessage(message));
370this.socketToApp.on("error", (error: Error) => {
371if (retryAttempt) {
372printDebuggingError(
373ErrorHelper.getInternalError(
374InternalErrorCode.ReconnectionToPackagerFailedCheckForErrorsOrRestartReactNative,
375),
376error,
377);
378}
379
380reject(error);
381});
32cab018Meena Kunnathur Balakrishnan10 years ago382
ce5e88eeYuri Skorokhodov5 years ago383// In an attempt to catch failures in starting the packager on first attempt,
384// wait for 300 ms before resolving the promise
259c018fYuri Skorokhodov5 years ago385PromiseUtil.delay(300).then(() => resolve());
ce5e88eeYuri Skorokhodov5 years ago386});
4677921cdigeff10 years ago387}
388
389private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago390return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago391}
392
ea8a5f88digeff10 years ago393private onSocketOpened() {
7cc67271digeff10 years ago394this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
34472878RedMickey5 years ago395logger.log(
396localize(
397"EstablishedConnectionWithPackagerToReactNativeApp",
398"Established a connection with the Proxy (Packager) to the React Native application",
399),
400),
401);
4677921cdigeff10 years ago402}
403
9174feb7Vladimir Kotikov9 years ago404private killWorker() {
405if (!this.singleLifetimeWorker) return;
406this.singleLifetimeWorker.stop();
407this.singleLifetimeWorker = null;
408}
e7b314e8Vladimir Kotikov9 years ago409
9174feb7Vladimir Kotikov9 years ago410private onMessage(message: string) {
5d4d4de0digeff10 years ago411try {
0a68f8dbArtem Egorov8 years ago412logger.verbose("From RN APP: " + message);
ea8a5f88digeff10 years ago413let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago414if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago415// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
416// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago417this.killWorker();
5d4d4de0digeff10 years ago418// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
419this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago420} else if (object.method === "$disconnected") {
421// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago422this.killWorker();
5d4d4de0digeff10 years ago423} else if (object.method) {
424// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago425if (this.singleLifetimeWorker) {
426this.singleLifetimeWorker.postMessage(object);
427}
5d4d4de0digeff10 years ago428} else {
ea8a5f88digeff10 years ago429// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
34472878RedMickey5 years ago430logger.verbose(
431`The react-native app sent a message without specifying a method: ${message}`,
432);
5d4d4de0digeff10 years ago433}
434} catch (exception) {
34472878RedMickey5 years ago435printDebuggingError(
436ErrorHelper.getInternalError(
437InternalErrorCode.FailedToProcessMessageFromReactNativeApp,
438message,
439),
440exception,
441);
4677921cdigeff10 years ago442}
443}
444
445private gotPrepareJSRuntime(message: any): void {
446// Create the sandbox, and replay that we finished processing the message
34472878RedMickey5 years ago447this.startNewWorkerLifetime().then(
448() => {
449this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
450},
451error =>
452printDebuggingError(
453ErrorHelper.getInternalError(
454InternalErrorCode.FailedToPrepareJSRuntimeEnvironment,
455message,
456),
457error,
458),
459);
4677921cdigeff10 years ago460}
461
ff7dce65digeff10 years ago462private sendMessageToApp(message: any): void {
5c8365a6Artem Egorov8 years ago463let stringified: string = "";
354c28a1digeff10 years ago464try {
465stringified = JSON.stringify(message);
d124bf0eYuri Skorokhodov7 years ago466logger.verbose(`To RN APP: ${stringified}`);
354c28a1digeff10 years ago467this.socketToApp.send(stringified);
468} catch (exception) {
34472878RedMickey5 years ago469let messageToShow = stringified || "" + message; // Try to show the stringified version, but show the toString if unavailable
470printDebuggingError(
471ErrorHelper.getInternalError(
472InternalErrorCode.FailedToSendMessageToTheReactNativeApp,
473messageToShow,
474),
475exception,
476);
354c28a1digeff10 years ago477}
4677921cdigeff10 years ago478}
479}