From c00abe53c3e24218afa694b2b58440403a102fd2 Mon Sep 17 00:00:00 2001 From: Nurzhan Bakibayev Date: Mon, 10 Jul 2017 06:15:52 -0700 Subject: [PATCH] Move inspector proxy out of the packager Reviewed By: pakoito Differential Revision: D5369099 fbshipit-source-id: ff806d784b70804181c5c54837352f66e45d1b9e --- .../DevSupport/RCTInspectorDevServerHelper.mm | 11 +- .../systeminfo/AndroidInfoHelpers.java | 2 +- local-cli/server/runServer.js | 7 - local-cli/server/util/inspectorProxy.js | 483 ------------------ 4 files changed, 5 insertions(+), 498 deletions(-) delete mode 100644 local-cli/server/util/inspectorProxy.js diff --git a/React/DevSupport/RCTInspectorDevServerHelper.mm b/React/DevSupport/RCTInspectorDevServerHelper.mm index aed042087..f0033a6a9 100644 --- a/React/DevSupport/RCTInspectorDevServerHelper.mm +++ b/React/DevSupport/RCTInspectorDevServerHelper.mm @@ -3,6 +3,7 @@ #if RCT_DEV #import +#import #import "RCTDefines.h" #import "RCTInspectorPackagerConnection.h" @@ -16,10 +17,8 @@ static NSString *getDebugServerHost(NSURL *bundleURL) host = @"localhost"; } - NSNumber *port = [bundleURL port]; - if (!port) { - port = @8081; // Packager default port - } + // Inspector Proxy is run on a separate port (from packager). + NSNumber *port = @8082; // this is consistent with the Android implementation, where http:// is the // hardcoded implicit scheme for the debug server. Note, packagerURL @@ -31,11 +30,9 @@ static NSString *getDebugServerHost(NSURL *bundleURL) static NSURL *getInspectorDeviceUrl(NSURL *bundleURL) { - // TODO: t19163919: figure out if there's a good way to get a friendly device - // name for the end user return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/inspector/device?name=%@", getDebugServerHost(bundleURL), - @""]]; + [[UIDevice currentDevice] name]]]; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java index 5d98fd7ec..b3dd6f1a7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java @@ -13,7 +13,7 @@ public class AndroidInfoHelpers { public static final String DEVICE_LOCALHOST = "localhost"; private static final int DEBUG_SERVER_HOST_PORT = 8081; - private static final int INSPECTOR_PROXY_PORT = 8081; + private static final int INSPECTOR_PROXY_PORT = 8082; private static boolean isRunningOnGenymotion() { return Build.FINGERPRINT.contains("vbox"); diff --git a/local-cli/server/runServer.js b/local-cli/server/runServer.js index 1541e0e5c..a75d6d5c5 100644 --- a/local-cli/server/runServer.js +++ b/local-cli/server/runServer.js @@ -13,7 +13,6 @@ 'use strict'; require('../../setupBabel')(); -const InspectorProxy = require('./util/inspectorProxy.js'); const ReactPackager = require('metro-bundler'); const Terminal = require('metro-bundler/src/lib/Terminal'); @@ -37,7 +36,6 @@ const openStackFrameInEditorMiddleware = require('./middleware/openStackFrameInE const path = require('path'); const statusPageMiddleware = require('./middleware/statusPageMiddleware.js'); const systraceProfileMiddleware = require('./middleware/systraceProfileMiddleware.js'); -const unless = require('./middleware/unless'); const webSocketProxy = require('./util/webSocketProxy.js'); import type {ConfigT} from '../util/Config'; @@ -69,7 +67,6 @@ function runServer( const packagerServer = getPackagerServer(args, config); startedCallback(packagerServer._reporter); - const inspectorProxy = new InspectorProxy(); const app = connect() .use(loadRawBodyMiddleware) .use(connect.compress()) @@ -83,9 +80,6 @@ function runServer( .use(systraceProfileMiddleware) .use(cpuProfilerMiddleware) .use(indexPageMiddleware) - .use( - unless('/inspector', inspectorProxy.processRequest.bind(inspectorProxy)), - ) .use(packagerServer.processRequest.bind(packagerServer)); args.projectRoots.forEach(root => app.use(connect.static(root))); @@ -115,7 +109,6 @@ function runServer( wsProxy = webSocketProxy.attachToServer(serverInstance, '/debugger-proxy'); ms = messageSocket.attachToServer(serverInstance, '/message'); - inspectorProxy.attachToServer(serverInstance, '/inspector'); readyCallback(packagerServer._reporter); }); // Disable any kind of automatic timeout behavior for incoming diff --git a/local-cli/server/util/inspectorProxy.js b/local-cli/server/util/inspectorProxy.js deleted file mode 100644 index d4675d8af..000000000 --- a/local-cli/server/util/inspectorProxy.js +++ /dev/null @@ -1,483 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow - * - * This file implements a multi-device proxy for the Chrome debugging protocol. Each device connects - * to the proxy over a single websocket connection that is able to multiplex messages to multiple - * Javascript VMs. An inspector instance running in Chrome can connect to a specific VM via this - * proxy. - * - * The connection to from device to the proxy uses a simple JSON-based protocol with the basic - * structure { event: '', payload: { ... }}. All events are send without acknowledgment - * except the 'getPages' event where the device responds with the available pages. Most events will - * be wrapped inspector events going to a specific 'page' on the device. - * - * See below for a diagram of how the devices, inspector(s) and proxy interact. - * +--------+ - * | Device | +------------+ - * | #1 | +---------+ |Chrome | - * | | | | Chrome debugging +------------+ - * +--+--+ | Custom | | protocol |Inspector | - * |VM|VM| |------+------->| Proxy |<----------+------| | - * +--+--+--+ | | | | | | - * | | | | +------------+ - * +--------+ | +---------+ | |Inspector | - * | Device |------+ +------| | - * | #2 | | | - * | | +------------+ - * +--+ | - * |VM| | - * +--+-----+ - */ -'use strict'; - -const flatMapArray = require('fbjs/lib/flatMapArray'); -const http = require('http'); -const nullthrows = require('fbjs/lib/nullthrows'); -const querystring = require('querystring'); - -const parseUrl = require('url').parse; -const WebSocket = require('ws'); - -const debug = require('debug')('RNP:InspectorProxy'); -const launchChrome = require('./launchChrome'); - -import type {Server as HTTPSServer} from 'https'; - -type DevicePage = { - id: string, - title: string, -}; - -type Page = { - id: string, - title: string, - description: string, - devtoolsFrontendUrl: string, - webSocketDebuggerUrl: string, - deviceId: string, - deviceName: string, -}; - -type Message = { - event?: Name, - payload?: Payload, -}; - -type WrappedEvent = Message<'wrappedEvent', { - pageId?: string, - wrappedEvent?: string, -}>; - -type ConnectEvent = Message<'connect', { - pageId?: string, -}>; - -type DisconnectEvent = Message<'disconnect', { - pageId?: string, -}>; - -type OpenEvent = Message<'open', { - pageId?: string, -}>; - -type GetPages = Message<'getPages', ?Array>; - -type Event = WrappedEvent | ConnectEvent | DisconnectEvent | GetPages; - -type Address = { - address: string, - port: number, -}; - -type Server = http.Server | HTTPSServer; - -const DEVICE_TIMEOUT = 30000; - -// FIXME: This is the url we want to use as it more closely matches the actual protocol we use. -// However, it's broken in Chrome 54+ due to it using 'KeyboardEvent.keyIdentifier'. -// const DEVTOOLS_URL_BASE = 'https://chrome-devtools-frontend.appspot.com/serve_rev/@178469/devtools.html?ws='; -const DEVTOOLS_URL_BASE = 'https://chrome-devtools-frontend.appspot.com/serve_file/@60cd6e859b9f557d2312f5bf532f6aec5f284980/inspector.html?ws='; - -class Device { - name: string; - - _id: string; - _socket: WebSocket; - _handlers: Map void>; - _connections: Map; - - constructor(id: string, name: string, socket: WebSocket) { - this.name = name; - this._id = id; - this._socket = socket; - this._handlers = new Map(); - this._connections = new Map(); - - this._socket.on('message', this._onMessage.bind(this)); - this._socket.on('close', this._onDeviceDisconnected.bind(this)); - } - - getPages(): Promise> { - return this._callMethod('getPages'); - } - - connect(pageId: string, socket: WebSocket) { - socket.on('message', (message: string) => { - if (!this._connections.has(pageId)) { - // Not connected, silently ignoring - return; - } - - // TODO: This should be handled way earlier, preferably in the inspector itself. - // That is how it works it newer versions but it requires installing hooks. - if (message.indexOf('Network.loadResourceForFrontend') !== -1) { - this._loadResourceForFrontend(socket, JSON.parse(message)); - return; - } - - this._send({ - event: 'wrappedEvent', - payload: { - pageId, - wrappedEvent: message, - }, - }); - }); - socket.on('close', () => { - if (this._connections.has(pageId)) { - this._send({event: 'disconnect', payload: {pageId: pageId}}); - this._removeConnection(pageId); - } - }); - this._connections.set(pageId, socket); - this._send({event: 'connect', payload: {pageId: pageId}}); - } - - _callMethod(name: 'getPages'): Promise { - const promise = new Promise((fulfill, reject) => { - const timerId = setTimeout(() => { - this._handlers.delete(name); - reject(new Error('Timeout waiting for device')); - }, DEVICE_TIMEOUT); - this._handlers.set(name, arg => { - clearTimeout(timerId); - fulfill(arg); - }); - }); - this._send({event: name}); - return promise; - } - - _send(message: Event) { - debug('-> device', this._id, message); - // This try/catch is unfortunate, but there is a small window where a message can be sent - // 1. after the socket is closed, and - // 2. before the callback for the 'close' event on the socket is run. - // Since we don't want the packager to crash in this situation, we have to guard against this. - try { - this._socket.send(JSON.stringify(message)); - } catch (err) { - debug('Error sending', err); - } - } - - _onMessage(json: string) { - debug('<- device', this._id, json); - const message = JSON.parse(json); - const handler = this._handlers.get(message.event); - if (handler) { - this._handlers.delete(message.event); - handler(message.payload); - return; - } - - if (message.event === 'wrappedEvent') { - this._handleWrappedEvent(message); - } else if (message.event === 'disconnect') { - this._handleDisconnect(message); - } else if (message.event === 'open') { - this._handleOpen(message); - } - } - - _handleWrappedEvent(event: WrappedEvent) { - const payload = nullthrows(event.payload); - const socket = this._connections.get(nullthrows(payload.pageId)); - if (!socket) { - console.error('Invalid pageId from device:', payload.pageId); - return; - } - socket.send(payload.wrappedEvent); - } - - _handleDisconnect(event: DisconnectEvent) { - const payload = nullthrows(event.payload); - const pageId = nullthrows(payload.pageId); - this._removeConnection(pageId); - } - - _handleOpen(event: OpenEvent) { - const payload = nullthrows(event.payload); - const pageId = nullthrows(payload.pageId); - const url = DEVTOOLS_URL_BASE + makeInspectorPageUrl(this._id, pageId); - launchChrome(url); - } - - _removeConnection(pageId: string) { - const socket = this._connections.get(pageId); - if (socket) { - this._connections.delete(pageId); - socket.close(); - } - } - - _onDeviceDisconnected() { - for (const pageId of this._connections.keys()) { - this._removeConnection(pageId); - } - } - - _loadResourceForFrontend(socket: WebSocket, event: Object) { - const id: number = nullthrows(event.id); - const url: string = nullthrows(nullthrows(event.params).url); - debug('loadResourceForFrontend:', url); - http.get(this._normalizeUrl(url), (response) => { - // $FlowFixMe callback is optional - response.setTimeout(0); - let data = ''; - response.on('data', (chunk) => { data += chunk; }); - response.on('end', () => { - socket.send(JSON.stringify({ - id: id, - result: { - statusCode: response.statusCode, - content: data, - responseHeaders: response.headers, - }, - })); - }); - response.on('error', (error) => { - console.error('Failed to get resource', error); - }); - }); - } - - _normalizeUrl(url: string): string { - return url.replace('http://10.0.3.2', 'http://localhost') - .replace('http://10.0.2.2', 'http://localhost'); - } -} - -class InspectorProxy { - _devices: Map; - _devicesCounter: number; - - constructor() { - this._devices = new Map(); - this._devicesCounter = 0; - } - - attachToServer(server: Server, pathPrefix: string) { - this._createPageHandler(server, pathPrefix + '/page'); - this._createDeviceHandler(server, pathPrefix + '/device'); - this._createPagesListHandler(server, pathPrefix + '/'); - this._createPagesJsonHandler(server, pathPrefix + '/json'); - } - - _makePage(server: Address, deviceId: string, deviceName: string, devicePage: DevicePage): Page { - const wsUrl = makeInspectorPageUrl(deviceId, devicePage.id); - return { - id: `${deviceId}-${devicePage.id}`, - title: devicePage.title, - description: '', - devtoolsFrontendUrl: DEVTOOLS_URL_BASE + wsUrl, - webSocketDebuggerUrl: `ws://${wsUrl}`, - deviceId, - deviceName, - }; - } - - _getPages(localAddress: Address): Promise> { - const promises = Array.from(this._devices.entries(), ([deviceId, device]) => { - return device.getPages().then((devicePages) => { - return devicePages.map(this._makePage.bind(this, localAddress, deviceId, device.name)); - }); - }); - - const flatMap = (arr) => flatMapArray(arr, (x) => x); - return Promise.all(promises).then(flatMap); - } - - processRequest(req: any, res: any, next: any) { - // TODO: Might wanna actually do the handling here - const endpoints = [ - '/inspector/', - '/inspector/page', - '/inspector/device', - '/inspector/json', - ]; - if (endpoints.indexOf(req.url) === -1) { - next(); - } - } - - _createDeviceHandler(server: Server, path: string) { - const wss = new WebSocket.Server({ - server, - path, - }); - wss.on('connection', (socket: WebSocket) => { - try { - const query = parseUrl(socket.upgradeReq.url, true).query || {}; - const deviceName = query.name || 'Unknown'; - debug('Got device connection:', deviceName); - const deviceId = String(this._devicesCounter++); - const device = new Device(deviceId, deviceName, socket); - this._devices.set(deviceId, device); - socket.on('close', () => { - this._devices.delete(deviceId); - }); - } catch (e) { - console.error(e); - socket.close(1011, e.message); - } - }); - } - - _createPageHandler(server: Server, path: string) { - const wss = new WebSocket.Server({ - server, - path, - }); - wss.on('connection', (socket: WebSocket) => { - try { - const url = parseUrl(socket.upgradeReq.url, false); - const { device, page } = querystring.parse( - querystring.unescape(nullthrows(url.query))); - if (device === undefined || page === undefined) { - throw Error('Must provide device and page'); - } - const deviceObject = this._devices.get(device); - if (!deviceObject) { - throw Error('Unknown device: ' + device); - } - - deviceObject.connect(page, socket); - } catch (e) { - console.error(e); - socket.close(1011, e.message); - } - }); - } - - _createPagesJsonHandler(server: Server, path: string) { - server.on('request', (request: http.IncomingMessage, response: http.ServerResponse) => { - if (request.url === path) { - this._getPages(server.address()).then((result: Array) => { - response.writeHead(200, {'Content-Type': 'application/json'}); - response.end(JSON.stringify(result)); - }, (error: Error) => { - response.writeHead(500); - response.end('Internal error: ' + error.toString()); - }); - } - }); - } - - _createPagesListHandler(server: Server, path: string) { - server.on('request', (request: http.IncomingMessage, response: http.ServerResponse) => { - if (request.url === path) { - this._getPages(server.address()).then((result: Array) => { - response.writeHead(200, {'Content-Type': 'text/html'}); - response.end(buildPagesHtml(result)); - }, (error: Error) => { - response.writeHead(500); - response.end('Internal error: ' + error.toString()); - }); - } - }); - } - -} - -function buildPagesHtml(pages: Array): string { - const pagesHtml = pages.map((page) => { - return escapeHtml` -
  • - - ${page.deviceName} / ${page.title} - -
  • - `; - }).join('\n'); - - return ` - - Pages - -

    Pages

    -
    -
      - ${pagesHtml} -
    - - - `; -} - -function escapeHtml(pieces: Array, ...substitutions: Array): string { - let result = pieces[0]; - for (let i = 0; i < substitutions.length; ++i) { - result += substitutions[i].replace(/[<&"'>]/g, escapeHtmlSpecialChar) + pieces[i + 1]; - } - - return result; -} - -function escapeHtmlSpecialChar(char: string): string { - return ( - char === '&' ? '&' : - char === '"' ? '"' : - char === "'" ? ''' : - char === '<' ? '<' : - char === '>' ? '>' : - char - ); -} - -function makeInspectorPageUrl(deviceId: string, pageId: string): string { - // The inspector frontend doesn't handle urlencoded params so we - // manually urlencode it and decode it on the other side in _createPageHandler - const query = querystring.escape(`device=${deviceId}&page=${pageId}`); - return `localhost:8081/inspector/page?${query}`; -} - -function attachToServer(server: http.Server, pathPrefix: string): InspectorProxy { - const proxy = new InspectorProxy(); - proxy.attachToServer(server, pathPrefix); - return proxy; -} - -if (!module.parent) { - console.info('Starting server'); - process.env.DEBUG = 'RNP:Inspector'; - const serverInstance = http.createServer().listen( - 8081, - 'localhost', - undefined, - function() { - attachToServer(serverInstance, '/inspector'); - } - ); - serverInstance.timeout = 0; -} - -// module.exports.attachToServer = attachToServer; -module.exports = InspectorProxy;