microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.4.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

485lines · 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";
5940f996RedMickey5 years ago5import * as vscode from "vscode";
ea8a5f88digeff10 years ago6import * as WebSocket from "ws";
e45838cbVladimir Kotikov9 years ago7import { EventEmitter } from "events";
0a68f8dbArtem Egorov8 years ago8import { ensurePackagerRunning } from "../common/packagerStatus";
34472878RedMickey5 years ago9import { ErrorHelper } from "../common/error/errorHelper";
a6562589RedMickey6 years ago10import { logger } from "vscode-debugadapter";
34472878RedMickey5 years ago11import { ExecutionsLimiter } from "../common/executionsLimiter";
b05b5086Vladimir Kotikov9 years ago12import { ForkedAppWorker } from "./forkedAppWorker";
cc70057dVladimir Kotikov9 years ago13import { ScriptImporter } from "./scriptImporter";
bb869343Serge Svekolnikov8 years ago14import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
d124bf0eYuri Skorokhodov7 years ago15import * as nls from "vscode-nls";
16import { InternalErrorCode } from "../common/error/internalErrorCode";
ce5e88eeYuri Skorokhodov5 years ago17import { FileSystem } from "../common/node/fileSystem";
18import { PromiseUtil } from "../common/node/promise";
34472878RedMickey5 years ago19nls.config({
20messageFormat: nls.MessageFormat.bundle,
21bundleFormat: nls.BundleFormat.standalone,
22})();
d124bf0eYuri Skorokhodov7 years ago23const localize = nls.loadMessageBundle();
4677921cdigeff10 years ago24
e45838cbVladimir Kotikov9 years ago25export interface RNAppMessage {
ea8a5f88digeff10 years ago26method: string;
e45838cbVladimir Kotikov9 years ago27url?: string;
ea8a5f88digeff10 years ago28// These objects have also other properties but that we don't currently use
29}
5d4d4de0digeff10 years ago30
e45838cbVladimir Kotikov9 years ago31export interface IDebuggeeWorker {
ce5e88eeYuri Skorokhodov5 years ago32start(): Promise<any>;
e45838cbVladimir Kotikov9 years ago33stop(): void;
34postMessage(message: RNAppMessage): void;
35}
36
1758f9a6Yuri Skorokhodov7 years ago37function printDebuggingError(error: Error, reason: any) {
34472878RedMickey5 years ago38const nestedError = ErrorHelper.getNestedError(
39error,
40InternalErrorCode.DebuggingWontWorkReloadJSAndReconnect,
41reason,
42);
0a68f8dbArtem Egorov8 years ago43
44logger.error(nestedError.message);
039239d1Vladimir Kotikov9 years ago45}
46
34472878RedMickey5 years ago47/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
48* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
49* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
50* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
51*/
039239d1Vladimir Kotikov9 years ago52
53export class MultipleLifetimesAppWorker extends EventEmitter {
54public static WORKER_BOOTSTRAP = `
cc70057dVladimir Kotikov9 years ago55// Initialize some variables before react-native code would access them
d64a6928Vladimir Kotikov9 years ago56var onmessage=null, self=global;
cc70057dVladimir Kotikov9 years ago57// Cache Node's original require as __debug__.require
d64a6928Vladimir Kotikov9 years ago58global.__debug__={require: require};
0864d702Ruslan Bikkinin7 years ago59// Prevent leaking process.versions from debugger process to
60// worker because pure React Native doesn't do that and some packages as js-md5 rely on this behavior
61Object.defineProperty(process, "versions", {
62value: undefined
63});
7daed3fcArtem Egorov8 years ago64
4cf8fdf4Yuri Skorokhodov6 years ago65// TODO: Replace by url.fileURLToPath method when Node 10 LTS become deprecated
66function fileUrlToPath(url) {
67if (process.platform === 'win32') {
68return url.toString().replace('file:///', '');
69} else {
70return url.toString().replace('file://', '');
71}
72}
73
2ecfbd20Yuri Skorokhodov7 years ago74function getNativeModules() {
75var NativeModules;
76try {
77// This approach is for old RN versions
78NativeModules = global.require('NativeModules');
79} catch (err) {
80// ignore error and try another way for more recent RN versions
81try {
82var nativeModuleId;
83var modules = global.__r.getModules();
84var ids = Object.keys(modules);
85for (var i = 0; i < ids.length; i++) {
86if (modules[ids[i]].verboseName) {
87var packagePath = new String(modules[ids[i]].verboseName);
c3481846Yuri Skorokhodov5 years ago88if (packagePath.indexOf('Libraries/BatchedBridge/NativeModules.js') > 0 || packagePath.indexOf('Libraries\\\\BatchedBridge\\\\NativeModules.js') > 0) {
2ecfbd20Yuri Skorokhodov7 years ago89nativeModuleId = parseInt(ids[i], 10);
90break;
91}
92}
93}
94if (nativeModuleId) {
95NativeModules = global.__r(nativeModuleId);
96}
97}
98catch (err) {
99// suppress errors
100}
101}
102return NativeModules;
103}
104
105// Originally, this was made for iOS only
7daed3fcArtem Egorov8 years ago106var vscodeHandlers = {
107'vscode_reloadApp': function () {
2ecfbd20Yuri Skorokhodov7 years ago108var NativeModules = getNativeModules();
4dfc9ffdRedMickey5 years ago109if (NativeModules && NativeModules.DevMenu) {
2ecfbd20Yuri Skorokhodov7 years ago110NativeModules.DevMenu.reload();
7daed3fcArtem Egorov8 years ago111}
112},
113'vscode_showDevMenu': function () {
2ecfbd20Yuri Skorokhodov7 years ago114var NativeModules = getNativeModules();
4dfc9ffdRedMickey5 years ago115if (NativeModules && NativeModules.DevMenu) {
2ecfbd20Yuri Skorokhodov7 years ago116NativeModules.DevMenu.show();
7daed3fcArtem Egorov8 years ago117}
118}
119};
120
121process.on("message", function (message) {
122if (message.data && vscodeHandlers[message.data.method]) {
123vscodeHandlers[message.data.method]();
124} else if(onmessage) {
125onmessage(message);
126}
cc70057dVladimir Kotikov9 years ago127});
7daed3fcArtem Egorov8 years ago128
cc70057dVladimir Kotikov9 years ago129var postMessage = function(message){
130process.send(message);
131};
bb869343Serge Svekolnikov8 years ago132
133if (!self.postMessage) {
134self.postMessage = postMessage;
135}
136
cc70057dVladimir Kotikov9 years ago137var importScripts = (function(){
138var fs=require('fs'), vm=require('vm');
139return function(scriptUrl){
4cf8fdf4Yuri Skorokhodov6 years ago140scriptUrl = fileUrlToPath(scriptUrl);
141var scriptCode = fs.readFileSync(scriptUrl, 'utf8');
e67ace8aYuri Skorokhodov6 years ago142// Add a 'debugger;' statement to stop code execution
143// to wait for the sourcemaps to be processed by the debug adapter
144vm.runInThisContext('debugger;' + scriptCode, {filename: scriptUrl});
cc70057dVladimir Kotikov9 years ago145};
4cf8fdf4Yuri Skorokhodov6 years ago146})();
147`;
cc70057dVladimir Kotikov9 years ago148
cf6dd6b9Yuri Skorokhodov7 years ago149public static CONSOLE_TRACE_PATCH = `// Worker is ran as nodejs process, so console.trace() writes to stderr and it leads to error in native app
150// To avoid this console.trace() is overridden to print stacktrace via console.log()
151// Please, see Node JS implementation: https://github.com/nodejs/node/blob/master/lib/internal/console/constructor.js
152console.trace = (function() {
153return function() {
154try {
155var err = {
156name: 'Trace',
157message: require('util').format.apply(null, arguments)
158};
159// Node uses 10, but usually it's not enough for RN app trace
160Error.stackTraceLimit = 30;
161Error.captureStackTrace(err, console.trace);
162console.log(err.stack);
163} catch (e) {
164console.error(e);
165}
166};
4cf8fdf4Yuri Skorokhodov6 years ago167})();
168`;
cf6dd6b9Yuri Skorokhodov7 years ago169
13a99427Yuri Skorokhodov6 years ago170public 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:
171// https://github.com/pubkey/broadcast-channel/blob/master/src/util.js#L64
172// To avoid it if process.toString() is called if will return empty string instead of [object process].
173var nativeObjectToString = Object.prototype.toString;
174Object.prototype.toString = function() {
175if (this === process) {
176return '';
177} else {
178return nativeObjectToString.call(this);
179}
f4e3ce39Yuri Skorokhodov6 years ago180};
13a99427Yuri Skorokhodov6 years ago181`;
182
039239d1Vladimir Kotikov9 years ago183public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago184// and started listening for IPC messages
185postMessage({workerLoaded:true});`;
186
bb869343Serge Svekolnikov8 years ago187public static FETCH_STUB = `(function(self) {
4cf8fdf4Yuri Skorokhodov6 years ago188'use strict';
bb869343Serge Svekolnikov8 years ago189
4cf8fdf4Yuri Skorokhodov6 years ago190if (self.fetch) {
191return;
192}
193
194self.fetch = fetch;
bb869343Serge Svekolnikov8 years ago195
4cf8fdf4Yuri Skorokhodov6 years ago196function fetch(url) {
197return new Promise((resolve, reject) => {
198var data = require('fs').readFileSync(fileUrlToPath(url), 'utf8');
199resolve(
200{
201text: function () {
202return data;
203}
bb869343Serge Svekolnikov8 years ago204});
4cf8fdf4Yuri Skorokhodov6 years ago205});
206}
207})(global);
208`;
bb869343Serge Svekolnikov8 years ago209
6eeec3c0Serge Svekolnikov8 years ago210private packagerAddress: string;
e3ae4227digeff10 years ago211private packagerPort: number;
4677921cdigeff10 years ago212private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago213private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago214private packagerRemoteRoot?: string;
215private packagerLocalRoot?: string;
cf911877Yuri Skorokhodov7 years ago216private debuggerWorkerUrlPath?: string;
ea8a5f88digeff10 years ago217private socketToApp: WebSocket;
5940f996RedMickey5 years ago218private cancellationToken: vscode.CancellationToken;
5c8365a6Artem Egorov8 years ago219private singleLifetimeWorker: IDebuggeeWorker | null;
3b6023b2Jimmy Thomson10 years ago220private webSocketConstructor: (url: string) => WebSocket;
221
7cc67271digeff10 years ago222private executionLimiter = new ExecutionsLimiter();
ce5e88eeYuri Skorokhodov5 years ago223private nodeFileSystem = new FileSystem();
cc70057dVladimir Kotikov9 years ago224private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago225
6eeec3c0Serge Svekolnikov8 years ago226constructor(
227attachRequestArguments: any,
228sourcesStoragePath: string,
229projectRootPath: string,
5940f996RedMickey5 years ago230cancellationToken: vscode.CancellationToken,
34472878RedMickey5 years ago231{ webSocketConstructor = (url: string) => new WebSocket(url) } = {},
232) {
e45838cbVladimir Kotikov9 years ago233super();
6eeec3c0Serge Svekolnikov8 years ago234this.packagerAddress = attachRequestArguments.address || "localhost";
235this.packagerPort = attachRequestArguments.port;
236this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
237this.packagerLocalRoot = attachRequestArguments.localRoot;
cf911877Yuri Skorokhodov7 years ago238this.debuggerWorkerUrlPath = attachRequestArguments.debuggerWorkerUrlPath;
4677921cdigeff10 years ago239this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago240this.projectRootPath = projectRootPath;
5940f996RedMickey5 years ago241this.cancellationToken = cancellationToken;
1758f9a6Yuri Skorokhodov7 years ago242if (!this.sourcesStoragePath)
243throw ErrorHelper.getInternalError(InternalErrorCode.SourcesStoragePathIsNullOrEmpty);
3b6023b2Jimmy Thomson10 years ago244this.webSocketConstructor = webSocketConstructor;
34472878RedMickey5 years ago245this.scriptImporter = new ScriptImporter(
246this.packagerAddress,
247this.packagerPort,
248sourcesStoragePath,
249this.packagerRemoteRoot,
250this.packagerLocalRoot,
251);
4677921cdigeff10 years ago252}
253
ce5e88eeYuri Skorokhodov5 years ago254public start(retryAttempt: boolean = false): Promise<any> {
34472878RedMickey5 years ago255const errPackagerNotRunning = ErrorHelper.getInternalError(
256InternalErrorCode.CannotAttachToPackagerCheckPackagerRunningOnPort,
257this.packagerPort,
258);
0a68f8dbArtem Egorov8 years ago259
6eeec3c0Serge Svekolnikov8 years ago260return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
cc70057dVladimir Kotikov9 years ago261.then(() => {
262// Don't fetch debugger worker on socket disconnect
34472878RedMickey5 years ago263return retryAttempt ? Promise.resolve() : this.downloadAndPatchDebuggerWorker();
cc70057dVladimir Kotikov9 years ago264})
265.then(() => this.createSocketToApp(retryAttempt));
ff7dce65digeff10 years ago266}
267
34472878RedMickey5 years ago268public stop(): void {
e45838cbVladimir Kotikov9 years ago269if (this.socketToApp) {
270this.socketToApp.removeAllListeners();
271this.socketToApp.close();
272}
273
274if (this.singleLifetimeWorker) {
275this.singleLifetimeWorker.stop();
276}
277}
278
ce5e88eeYuri Skorokhodov5 years ago279public downloadAndPatchDebuggerWorker(): Promise<void> {
34472878RedMickey5 years ago280let scriptToRunPath = path.resolve(
281this.sourcesStoragePath,
282ScriptImporter.DEBUGGER_WORKER_FILENAME,
283);
284return this.scriptImporter
285.downloadDebuggerWorker(
286this.sourcesStoragePath,
287this.projectRootPath,
288this.debuggerWorkerUrlPath,
289)
cc70057dVladimir Kotikov9 years ago290.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
291.then((workerContent: string) => {
bb869343Serge Svekolnikov8 years ago292const isHaulProject = ReactNativeProjectHelper.isHaulProject(this.projectRootPath);
cc70057dVladimir Kotikov9 years ago293// Add our customizations to debugger worker to get it working smoothly
294// in Node env and polyfill WebWorkers API over Node's IPC.
bb869343Serge Svekolnikov8 years ago295const modifiedDebuggeeContent = [
296MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
cf6dd6b9Yuri Skorokhodov7 years ago297MultipleLifetimesAppWorker.CONSOLE_TRACE_PATCH,
13a99427Yuri Skorokhodov6 years ago298MultipleLifetimesAppWorker.PROCESS_TO_STRING_PATCH,
bb869343Serge Svekolnikov8 years ago299isHaulProject ? MultipleLifetimesAppWorker.FETCH_STUB : null,
300workerContent,
301MultipleLifetimesAppWorker.WORKER_DONE,
302].join("\n");
cc70057dVladimir Kotikov9 years ago303return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
304});
305}
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
ce5e88eeYuri Skorokhodov5 years ago323private 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.");
34472878RedMickey5 years ago336return this.singleLifetimeWorker.start().then(startedEvent => {
337this.emit("connected", startedEvent);
338});
4677921cdigeff10 years ago339}
340
ce5e88eeYuri Skorokhodov5 years ago341private createSocketToApp(retryAttempt: boolean = false): Promise<void> {
342return new Promise((resolve, reject) => {
343this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
344this.socketToApp.on("open", () => {
345this.onSocketOpened();
299b0557Patricio Beltran10 years ago346});
34472878RedMickey5 years ago347this.socketToApp.on("close", () => {
348this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
349/*
350* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
351* it closes the socket because it already has a connection to a debugger.
352* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
353*/
354let msgKey = "_closeMessage";
355if (this.socketToApp[msgKey] === "Another debugger is already connected") {
356reject(
357ErrorHelper.getInternalError(
358InternalErrorCode.AnotherDebuggerConnectedToPackager,
359),
360);
ce5e88eeYuri Skorokhodov5 years ago361}
34472878RedMickey5 years ago362logger.log(
363localize(
364"DisconnectedFromThePackagerToReactNative",
365"Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...",
366),
367);
ce5e88eeYuri Skorokhodov5 years ago368});
5940f996RedMickey5 years ago369if (!this.cancellationToken.isCancellationRequested) {
370setTimeout(() => {
371this.start(true /* retryAttempt */);
372}, 100);
373}
34472878RedMickey5 years ago374});
375this.socketToApp.on("message", (message: any) => this.onMessage(message));
376this.socketToApp.on("error", (error: Error) => {
377if (retryAttempt) {
378printDebuggingError(
379ErrorHelper.getInternalError(
380InternalErrorCode.ReconnectionToPackagerFailedCheckForErrorsOrRestartReactNative,
381),
382error,
383);
384}
385
386reject(error);
387});
32cab018Meena Kunnathur Balakrishnan10 years ago388
ce5e88eeYuri Skorokhodov5 years ago389// In an attempt to catch failures in starting the packager on first attempt,
390// wait for 300 ms before resolving the promise
259c018fYuri Skorokhodov5 years ago391PromiseUtil.delay(300).then(() => resolve());
ce5e88eeYuri Skorokhodov5 years ago392});
4677921cdigeff10 years ago393}
394
395private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago396return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago397}
398
ea8a5f88digeff10 years ago399private onSocketOpened() {
7cc67271digeff10 years ago400this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
34472878RedMickey5 years ago401logger.log(
402localize(
403"EstablishedConnectionWithPackagerToReactNativeApp",
404"Established a connection with the Proxy (Packager) to the React Native application",
405),
406),
407);
4677921cdigeff10 years ago408}
409
9174feb7Vladimir Kotikov9 years ago410private killWorker() {
411if (!this.singleLifetimeWorker) return;
412this.singleLifetimeWorker.stop();
413this.singleLifetimeWorker = null;
414}
e7b314e8Vladimir Kotikov9 years ago415
9174feb7Vladimir Kotikov9 years ago416private onMessage(message: string) {
5d4d4de0digeff10 years ago417try {
0a68f8dbArtem Egorov8 years ago418logger.verbose("From RN APP: " + message);
ea8a5f88digeff10 years ago419let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago420if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago421// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
422// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago423this.killWorker();
5d4d4de0digeff10 years ago424// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
425this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago426} else if (object.method === "$disconnected") {
427// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago428this.killWorker();
5d4d4de0digeff10 years ago429} else if (object.method) {
430// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago431if (this.singleLifetimeWorker) {
432this.singleLifetimeWorker.postMessage(object);
433}
5d4d4de0digeff10 years ago434} else {
ea8a5f88digeff10 years ago435// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
34472878RedMickey5 years ago436logger.verbose(
437`The react-native app sent a message without specifying a method: ${message}`,
438);
5d4d4de0digeff10 years ago439}
440} catch (exception) {
34472878RedMickey5 years ago441printDebuggingError(
442ErrorHelper.getInternalError(
443InternalErrorCode.FailedToProcessMessageFromReactNativeApp,
444message,
445),
446exception,
447);
4677921cdigeff10 years ago448}
449}
450
451private gotPrepareJSRuntime(message: any): void {
452// Create the sandbox, and replay that we finished processing the message
34472878RedMickey5 years ago453this.startNewWorkerLifetime().then(
454() => {
455this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
456},
457error =>
458printDebuggingError(
459ErrorHelper.getInternalError(
460InternalErrorCode.FailedToPrepareJSRuntimeEnvironment,
461message,
462),
463error,
464),
465);
4677921cdigeff10 years ago466}
467
ff7dce65digeff10 years ago468private sendMessageToApp(message: any): void {
5c8365a6Artem Egorov8 years ago469let stringified: string = "";
354c28a1digeff10 years ago470try {
471stringified = JSON.stringify(message);
d124bf0eYuri Skorokhodov7 years ago472logger.verbose(`To RN APP: ${stringified}`);
354c28a1digeff10 years ago473this.socketToApp.send(stringified);
474} catch (exception) {
34472878RedMickey5 years ago475let messageToShow = stringified || "" + message; // Try to show the stringified version, but show the toString if unavailable
476printDebuggingError(
477ErrorHelper.getInternalError(
478InternalErrorCode.FailedToSendMessageToTheReactNativeApp,
479messageToShow,
480),
481exception,
482);
354c28a1digeff10 years ago483}
4677921cdigeff10 years ago484}
485}