microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.5.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/appWorker.ts

278lines · 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
4677921cdigeff10 years ago4import * as Q from "q";
cc70057dVladimir Kotikov9 years ago5import * as path from "path";
ea8a5f88digeff10 years ago6import * as WebSocket from "ws";
e45838cbVladimir Kotikov9 years ago7import { EventEmitter } from "events";
0a68f8dbArtem Egorov8 years ago8import { ensurePackagerRunning } from "../common/packagerStatus";
a4a7e387Meena Kunnathur Balakrishnan10 years ago9import {ErrorHelper} from "../common/error/errorHelper";
0a68f8dbArtem Egorov8 years ago10import { logger } from "vscode-chrome-debug-core";
7cc67271digeff10 years ago11import {ExecutionsLimiter} from "../common/executionsLimiter";
cc70057dVladimir Kotikov9 years ago12import { FileSystem as NodeFileSystem} from "../common/node/fileSystem";
b05b5086Vladimir Kotikov9 years ago13import { ForkedAppWorker } from "./forkedAppWorker";
cc70057dVladimir Kotikov9 years ago14import { ScriptImporter } from "./scriptImporter";
4677921cdigeff10 years ago15
e45838cbVladimir Kotikov9 years ago16export interface RNAppMessage {
ea8a5f88digeff10 years ago17method: string;
e45838cbVladimir Kotikov9 years ago18url?: string;
ea8a5f88digeff10 years ago19// These objects have also other properties but that we don't currently use
20}
5d4d4de0digeff10 years ago21
e45838cbVladimir Kotikov9 years ago22export interface IDebuggeeWorker {
23start(): Q.Promise<any>;
24stop(): void;
25postMessage(message: RNAppMessage): void;
26}
27
039239d1Vladimir Kotikov9 years ago28function printDebuggingError(message: string, reason: any) {
0a68f8dbArtem Egorov8 years ago29const nestedError = ErrorHelper.getNestedWarning(reason, `${message}. Debugging won't work: Try reloading the JS from inside the app, or Reconnect the VS Code debugger`);
30
31logger.error(nestedError.message);
039239d1Vladimir Kotikov9 years ago32}
33
34/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
35* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
36* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
37* When the socket closes, we'll create a new SandboxedAppWorker and a new socket pair and discard the old ones.
38*/
39
40export class MultipleLifetimesAppWorker extends EventEmitter {
41public static WORKER_BOOTSTRAP = `
cc70057dVladimir Kotikov9 years ago42// Initialize some variables before react-native code would access them
d64a6928Vladimir Kotikov9 years ago43var onmessage=null, self=global;
cc70057dVladimir Kotikov9 years ago44// Cache Node's original require as __debug__.require
d64a6928Vladimir Kotikov9 years ago45global.__debug__={require: require};
46// avoid Node's GLOBAL deprecation warning
47Object.defineProperty(global, "GLOBAL", {
48configurable: true,
49writable: true,
50enumerable: true,
51value: global
52});
7daed3fcArtem Egorov8 years ago53
54var vscodeHandlers = {
55'vscode_reloadApp': function () {
56try {
57global.require('NativeModules').DevMenu.reload();
58} catch (err) {
59// ignore
60}
61},
62'vscode_showDevMenu': function () {
63try {
64var DevMenu = global.require('NativeModules').DevMenu.show();
65} catch (err) {
66// ignore
67}
68}
69};
70
71process.on("message", function (message) {
72if (message.data && vscodeHandlers[message.data.method]) {
73vscodeHandlers[message.data.method]();
74} else if(onmessage) {
75onmessage(message);
76}
cc70057dVladimir Kotikov9 years ago77});
7daed3fcArtem Egorov8 years ago78
cc70057dVladimir Kotikov9 years ago79var postMessage = function(message){
80process.send(message);
81};
82var importScripts = (function(){
83var fs=require('fs'), vm=require('vm');
84return function(scriptUrl){
85var scriptCode = fs.readFileSync(scriptUrl, "utf8");
86vm.runInThisContext(scriptCode, {filename: scriptUrl});
87};
88})();`;
89
039239d1Vladimir Kotikov9 years ago90public static WORKER_DONE = `// Notify debugger that we're done with loading
cc70057dVladimir Kotikov9 years ago91// and started listening for IPC messages
92postMessage({workerLoaded:true});`;
93
6eeec3c0Serge Svekolnikov8 years ago94private packagerAddress: string;
e3ae4227digeff10 years ago95private packagerPort: number;
4677921cdigeff10 years ago96private sourcesStoragePath: string;
7daed3fcArtem Egorov8 years ago97private projectRootPath: string;
6eeec3c0Serge Svekolnikov8 years ago98private packagerRemoteRoot?: string;
99private packagerLocalRoot?: string;
ea8a5f88digeff10 years ago100private socketToApp: WebSocket;
5c8365a6Artem Egorov8 years ago101private singleLifetimeWorker: IDebuggeeWorker | null;
3b6023b2Jimmy Thomson10 years ago102private webSocketConstructor: (url: string) => WebSocket;
103
7cc67271digeff10 years ago104private executionLimiter = new ExecutionsLimiter();
cc70057dVladimir Kotikov9 years ago105private nodeFileSystem = new NodeFileSystem();
106private scriptImporter: ScriptImporter;
7cc67271digeff10 years ago107
6eeec3c0Serge Svekolnikov8 years ago108constructor(
109attachRequestArguments: any,
110sourcesStoragePath: string,
111projectRootPath: string,
112{
113webSocketConstructor = (url: string) => new WebSocket(url),
114} = {}) {
e45838cbVladimir Kotikov9 years ago115super();
6eeec3c0Serge Svekolnikov8 years ago116this.packagerAddress = attachRequestArguments.address || "localhost";
117this.packagerPort = attachRequestArguments.port;
118this.packagerRemoteRoot = attachRequestArguments.remoteRoot;
119this.packagerLocalRoot = attachRequestArguments.localRoot;
4677921cdigeff10 years ago120this.sourcesStoragePath = sourcesStoragePath;
7daed3fcArtem Egorov8 years ago121this.projectRootPath = projectRootPath;
ea8a5f88digeff10 years ago122console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
3b6023b2Jimmy Thomson10 years ago123
124this.webSocketConstructor = webSocketConstructor;
6eeec3c0Serge Svekolnikov8 years ago125this.scriptImporter = new ScriptImporter(this.packagerAddress, this.packagerPort, sourcesStoragePath, this.packagerRemoteRoot, this.packagerLocalRoot);
4677921cdigeff10 years ago126}
127
cc70057dVladimir Kotikov9 years ago128public start(retryAttempt: boolean = false): Q.Promise<any> {
0a68f8dbArtem Egorov8 years ago129const errPackagerNotRunning = new Error(`Cannot attach to packager. Are you sure there is a packager and it is running in the port ${this.packagerPort}? If your packager is configured to run in another port make sure to add that to the setting.json.`);
130
6eeec3c0Serge Svekolnikov8 years ago131return ensurePackagerRunning(this.packagerAddress, this.packagerPort, errPackagerNotRunning)
cc70057dVladimir Kotikov9 years ago132.then(() => {
133// Don't fetch debugger worker on socket disconnect
134return retryAttempt ? Q.resolve<void>(void 0) :
135this.downloadAndPatchDebuggerWorker();
136})
137.then(() => this.createSocketToApp(retryAttempt));
ff7dce65digeff10 years ago138}
139
e45838cbVladimir Kotikov9 years ago140public stop() {
141if (this.socketToApp) {
142this.socketToApp.removeAllListeners();
143this.socketToApp.close();
144}
145
146if (this.singleLifetimeWorker) {
147this.singleLifetimeWorker.stop();
148}
149}
150
cc70057dVladimir Kotikov9 years ago151public downloadAndPatchDebuggerWorker(): Q.Promise<void> {
152let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
a1324704Artem Egorov8 years ago153return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath, this.projectRootPath)
cc70057dVladimir Kotikov9 years ago154.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
155.then((workerContent: string) => {
156// Add our customizations to debugger worker to get it working smoothly
157// in Node env and polyfill WebWorkers API over Node's IPC.
039239d1Vladimir Kotikov9 years ago158const modifiedDebuggeeContent = [MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
159workerContent, MultipleLifetimesAppWorker.WORKER_DONE].join("\n");
cc70057dVladimir Kotikov9 years ago160return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
161});
162}
163
ff7dce65digeff10 years ago164private startNewWorkerLifetime(): Q.Promise<void> {
6eeec3c0Serge Svekolnikov8 years ago165this.singleLifetimeWorker = new ForkedAppWorker(this.packagerAddress, this.packagerPort, this.sourcesStoragePath, this.projectRootPath,
166(message) => {
167this.sendMessageToApp(message);
168},
169this.packagerRemoteRoot, this.packagerLocalRoot);
0a68f8dbArtem Egorov8 years ago170logger.verbose("A new app worker lifetime was created.");
e45838cbVladimir Kotikov9 years ago171return this.singleLifetimeWorker.start()
cc70057dVladimir Kotikov9 years ago172.then(startedEvent => {
173this.emit("connected", startedEvent);
174});
4677921cdigeff10 years ago175}
176
cc70057dVladimir Kotikov9 years ago177private createSocketToApp(retryAttempt: boolean = false): Q.Promise<void> {
b9356af0Meena Kunnathur Balakrishnan10 years ago178let deferred = Q.defer<void>();
6e731058Jimmy Thomson10 years ago179this.socketToApp = this.webSocketConstructor(this.debuggerProxyUrl());
b9356af0Meena Kunnathur Balakrishnan10 years ago180this.socketToApp.on("open", () => {
181this.onSocketOpened();
182});
299b0557Patricio Beltran10 years ago183this.socketToApp.on("close",
184() => {
185this.executionLimiter.execute("onSocketClose.msg", /*limitInSeconds*/ 10, () => {
186/*
187* It is not the best idea to compare with the message, but this is the only thing React Native gives that is unique when
188* it closes the socket because it already has a connection to a debugger.
189* https://github.com/facebook/react-native/blob/588f01e9982775f0699c7bfd56623d4ed3949810/local-cli/server/util/webSocketProxy.js#L38
190*/
6d5c8798Nikita Matrosov9 years ago191let msgKey = "_closeMessage";
192if (this.socketToApp[msgKey] === "Another debugger is already connected") {
299b0557Patricio Beltran10 years ago193deferred.reject(new RangeError("Another debugger is already connected to packager. Please close it before trying to debug with VSCode."));
194}
0a68f8dbArtem Egorov8 years ago195logger.log("Disconnected from the Proxy (Packager) to the React Native application. Retrying reconnection soon...");
299b0557Patricio Beltran10 years ago196});
197setTimeout(() => {
198this.start(true /* retryAttempt */);
199}, 100);
200});
b9356af0Meena Kunnathur Balakrishnan10 years ago201this.socketToApp.on("message",
ea8a5f88digeff10 years ago202(message: any) => this.onMessage(message));
b9356af0Meena Kunnathur Balakrishnan10 years ago203this.socketToApp.on("error",
32cab018Meena Kunnathur Balakrishnan10 years ago204(error: Error) => {
cc70057dVladimir Kotikov9 years ago205if (retryAttempt) {
0a68f8dbArtem Egorov8 years ago206printDebuggingError("Reconnection to the proxy (Packager) failed. Please check the output window for Packager errors, if any. If failure persists, please restart the React Native debugger.", error);
499fe4ebMeena Kunnathur Balakrishnan10 years ago207}
208
32cab018Meena Kunnathur Balakrishnan10 years ago209deferred.reject(error);
210});
211
499fe4ebMeena Kunnathur Balakrishnan10 years ago212// In an attempt to catch failures in starting the packager on first attempt,
213// wait for 300 ms before resolving the promise
b9356af0Meena Kunnathur Balakrishnan10 years ago214Q.delay(300).done(() => deferred.resolve(void 0));
32cab018Meena Kunnathur Balakrishnan10 years ago215return deferred.promise;
4677921cdigeff10 years ago216}
217
218private debuggerProxyUrl() {
6eeec3c0Serge Svekolnikov8 years ago219return `ws://${this.packagerAddress}:${this.packagerPort}/debugger-proxy?role=debugger&name=vscode`;
4677921cdigeff10 years ago220}
221
ea8a5f88digeff10 years ago222private onSocketOpened() {
7cc67271digeff10 years ago223this.executionLimiter.execute("onSocketOpened.msg", /*limitInSeconds*/ 10, () =>
0a68f8dbArtem Egorov8 years ago224logger.log("Established a connection with the Proxy (Packager) to the React Native application"));
4677921cdigeff10 years ago225}
226
9174feb7Vladimir Kotikov9 years ago227private killWorker() {
228if (!this.singleLifetimeWorker) return;
229this.singleLifetimeWorker.stop();
230this.singleLifetimeWorker = null;
231}
e7b314e8Vladimir Kotikov9 years ago232
9174feb7Vladimir Kotikov9 years ago233private onMessage(message: string) {
5d4d4de0digeff10 years ago234try {
0a68f8dbArtem Egorov8 years ago235logger.verbose("From RN APP: " + message);
ea8a5f88digeff10 years ago236let object = <RNAppMessage>JSON.parse(message);
5d4d4de0digeff10 years ago237if (object.method === "prepareJSRuntime") {
e7b314e8Vladimir Kotikov9 years ago238// In RN 0.40 Android runtime doesn't seem to be sending "$disconnected" event
239// when user reloads an app, hence we need to try to kill it here either.
9174feb7Vladimir Kotikov9 years ago240this.killWorker();
5d4d4de0digeff10 years ago241// The MultipleLifetimesAppWorker will handle prepareJSRuntime aka create new lifetime
242this.gotPrepareJSRuntime(object);
ff7dce65digeff10 years ago243} else if (object.method === "$disconnected") {
244// We need to shutdown the current app worker, and create a new lifetime
9174feb7Vladimir Kotikov9 years ago245this.killWorker();
5d4d4de0digeff10 years ago246} else if (object.method) {
247// All the other messages are handled by the single lifetime worker
5c8365a6Artem Egorov8 years ago248if (this.singleLifetimeWorker) {
249this.singleLifetimeWorker.postMessage(object);
250}
5d4d4de0digeff10 years ago251} else {
ea8a5f88digeff10 years ago252// Message doesn't have a method. Ignore it. This is an info message instead of warn because it's normal and expected
0a68f8dbArtem Egorov8 years ago253logger.verbose("The react-native app sent a message without specifying a method: " + message);
5d4d4de0digeff10 years ago254}
255} catch (exception) {
ce591c62digeff10 years ago256printDebuggingError(`Failed to process message from the React Native app. Message:\n${message}`, exception);
4677921cdigeff10 years ago257}
258}
259
260private gotPrepareJSRuntime(message: any): void {
261// Create the sandbox, and replay that we finished processing the message
ff7dce65digeff10 years ago262this.startNewWorkerLifetime().done(() => {
263this.sendMessageToApp({ replyID: parseInt(message.id, 10) });
264}, error => printDebuggingError(`Failed to prepare the JavaScript runtime environment. Message:\n${message}`, error));
4677921cdigeff10 years ago265}
266
ff7dce65digeff10 years ago267private sendMessageToApp(message: any): void {
5c8365a6Artem Egorov8 years ago268let stringified: string = "";
354c28a1digeff10 years ago269try {
270stringified = JSON.stringify(message);
0a68f8dbArtem Egorov8 years ago271logger.verbose("To RN APP: " + stringified);
354c28a1digeff10 years ago272this.socketToApp.send(stringified);
273} catch (exception) {
274let messageToShow = stringified || ("" + message); // Try to show the stringified version, but show the toString if unavailable
ce591c62digeff10 years ago275printDebuggingError(`Failed to send message to the React Native app. Message:\n${messageToShow}`, exception);
354c28a1digeff10 years ago276}
4677921cdigeff10 years ago277}
278}