yeet
This commit is contained in:
459
node_modules/metro-inspector-proxy/src/Device.js
generated
vendored
Normal file
459
node_modules/metro-inspector-proxy/src/Device.js
generated
vendored
Normal file
@ -0,0 +1,459 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
var _ws = _interopRequireDefault(require("ws"));
|
||||
|
||||
function _interopRequireDefault(obj) {
|
||||
return obj && obj.__esModule ? obj : { default: obj };
|
||||
}
|
||||
|
||||
function _defineProperty(obj, key, value) {
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
const PAGES_POLLING_INTERVAL = 1000;
|
||||
|
||||
const debug = require("debug")("Metro:InspectorProxy"); // Android's stock emulator and other emulators such as genymotion use a standard localhost alias.
|
||||
|
||||
const EMULATOR_LOCALHOST_ADDRESSES = ["10.0.2.2", "10.0.3.2"]; // Prefix for script URLs that are alphanumeric IDs. See comment in _processMessageFromDevice method for
|
||||
// more details.
|
||||
|
||||
const FILE_PREFIX = "file://";
|
||||
const REACT_NATIVE_RELOADABLE_PAGE = {
|
||||
id: "-1",
|
||||
title: "React Native Experimental (Improved Chrome Reloads)",
|
||||
vm: "don't use",
|
||||
app: "don't use"
|
||||
};
|
||||
/**
|
||||
* Device class represents single device connection to Inspector Proxy. Each device
|
||||
* can have multiple inspectable pages.
|
||||
*/
|
||||
|
||||
class Device {
|
||||
// ID of the device.
|
||||
// Name of the device.
|
||||
// Package name of the app.
|
||||
// Stores socket connection between Inspector Proxy and device.
|
||||
// Stores last list of device's pages.
|
||||
// Stores information about currently connected debugger (if any).
|
||||
// Last known Page ID of the React Native page.
|
||||
// This is used by debugger connections that don't have PageID specified
|
||||
// (and will interact with the latest React Native page).
|
||||
// Whether we are in the middle of a reload in the REACT_NATIVE_RELOADABLE_PAGE.
|
||||
// The previous "GetPages" message, for deduplication in debug logs.
|
||||
constructor(id, name, app, socket) {
|
||||
_defineProperty(this, "_debuggerConnection", null);
|
||||
|
||||
_defineProperty(this, "_lastReactNativePageId", null);
|
||||
|
||||
_defineProperty(this, "_isReloading", false);
|
||||
|
||||
_defineProperty(this, "_lastGetPagesMessage", "");
|
||||
|
||||
this._id = id;
|
||||
this._name = name;
|
||||
this._app = app;
|
||||
this._pages = [];
|
||||
this._deviceSocket = socket;
|
||||
|
||||
this._deviceSocket.on("message", message => {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
|
||||
if (parsedMessage.event === "getPages") {
|
||||
// There's a 'getPages' message every second, so only show them if they change
|
||||
if (message !== this._lastGetPagesMessage) {
|
||||
debug(
|
||||
"(Debugger) (Proxy) <- (Device), getPages ping has changed: " +
|
||||
message
|
||||
);
|
||||
this._lastGetPagesMessage = message;
|
||||
}
|
||||
} else {
|
||||
debug("(Debugger) (Proxy) <- (Device): " + message);
|
||||
}
|
||||
|
||||
this._handleMessageFromDevice(parsedMessage);
|
||||
});
|
||||
|
||||
this._deviceSocket.on("close", () => {
|
||||
// Device disconnected - close debugger connection.
|
||||
if (this._debuggerConnection) {
|
||||
this._debuggerConnection.socket.close();
|
||||
|
||||
this._debuggerConnection = null;
|
||||
}
|
||||
});
|
||||
|
||||
this._setPagesPolling();
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
getPagesList() {
|
||||
if (this._lastReactNativePageId) {
|
||||
return this._pages.concat(REACT_NATIVE_RELOADABLE_PAGE);
|
||||
} else {
|
||||
return this._pages;
|
||||
}
|
||||
} // Handles new debugger connection to this device:
|
||||
// 1. Sends connect event to device
|
||||
// 2. Forwards all messages from the debugger to device as wrappedEvent
|
||||
// 3. Sends disconnect event to device when debugger connection socket closes.
|
||||
|
||||
handleDebuggerConnection(socket, pageId) {
|
||||
// Disconnect current debugger if we already have debugger connected.
|
||||
if (this._debuggerConnection) {
|
||||
this._debuggerConnection.socket.close();
|
||||
|
||||
this._debuggerConnection = null;
|
||||
}
|
||||
|
||||
const debuggerInfo = {
|
||||
socket,
|
||||
prependedFilePrefix: false,
|
||||
pageId
|
||||
};
|
||||
this._debuggerConnection = debuggerInfo;
|
||||
debug(`Got new debugger connection for page ${pageId} of ${this._name}`);
|
||||
|
||||
this._sendMessageToDevice({
|
||||
event: "connect",
|
||||
payload: {
|
||||
pageId: this._getPageId(pageId)
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("message", message => {
|
||||
debug("(Debugger) -> (Proxy) (Device): " + message);
|
||||
const parsedMessage = JSON.parse(message);
|
||||
|
||||
this._processMessageFromDebugger(parsedMessage, debuggerInfo);
|
||||
|
||||
this._sendMessageToDevice({
|
||||
event: "wrappedEvent",
|
||||
payload: {
|
||||
pageId: this._getPageId(pageId),
|
||||
wrappedEvent: JSON.stringify(parsedMessage)
|
||||
}
|
||||
});
|
||||
});
|
||||
socket.on("close", () => {
|
||||
debug(`Debugger for page ${pageId} and ${this._name} disconnected.`);
|
||||
|
||||
this._sendMessageToDevice({
|
||||
event: "disconnect",
|
||||
payload: {
|
||||
pageId: this._getPageId(pageId)
|
||||
}
|
||||
});
|
||||
|
||||
this._debuggerConnection = null;
|
||||
});
|
||||
const sendFunc = socket.send;
|
||||
|
||||
socket.send = function(message) {
|
||||
debug("(Debugger) <- (Proxy) (Device): " + message);
|
||||
return sendFunc.call(socket, message);
|
||||
};
|
||||
} // Handles messages received from device:
|
||||
// 1. For getPages responses updates local _pages list.
|
||||
// 2. All other messages are forwarded to debugger as wrappedEvent.
|
||||
//
|
||||
// In the future more logic will be added to this method for modifying
|
||||
// some of the messages (like updating messages with source maps and file
|
||||
// locations).
|
||||
|
||||
_handleMessageFromDevice(message) {
|
||||
if (message.event === "getPages") {
|
||||
this._pages = message.payload; // Check if device have new React Native page.
|
||||
// There is usually no more than 2-3 pages per device so this operation
|
||||
// is not expensive.
|
||||
// TODO(hypuk): It is better for VM to send update event when new page is
|
||||
// created instead of manually checking this on every getPages result.
|
||||
|
||||
for (let i = 0; i < this._pages.length; ++i) {
|
||||
if (this._pages[i].title.indexOf("React") >= 0) {
|
||||
if (this._pages[i].id != this._lastReactNativePageId) {
|
||||
this._newReactNativePage(this._pages[i].id);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (message.event === "disconnect") {
|
||||
// Device sends disconnect events only when page is reloaded or
|
||||
// if debugger socket was disconnected.
|
||||
const pageId = message.payload.pageId;
|
||||
const debuggerSocket = this._debuggerConnection
|
||||
? this._debuggerConnection.socket
|
||||
: null;
|
||||
|
||||
if (debuggerSocket && debuggerSocket.readyState === _ws.default.OPEN) {
|
||||
if (
|
||||
this._debuggerConnection != null &&
|
||||
this._debuggerConnection.pageId !== REACT_NATIVE_RELOADABLE_PAGE.id
|
||||
) {
|
||||
debug(`Page ${pageId} is reloading.`);
|
||||
debuggerSocket.send(
|
||||
JSON.stringify({
|
||||
method: "reload"
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (message.event === "wrappedEvent") {
|
||||
if (this._debuggerConnection == null) {
|
||||
return;
|
||||
} // FIXME: Is it possible that we received message for pageID that does not
|
||||
// correspond to current debugger connection?
|
||||
|
||||
const debuggerSocket = this._debuggerConnection.socket;
|
||||
|
||||
if (
|
||||
debuggerSocket == null ||
|
||||
debuggerSocket.readyState !== _ws.default.OPEN
|
||||
) {
|
||||
// TODO(hypuk): Send error back to device?
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedPayload = JSON.parse(message.payload.wrappedEvent);
|
||||
|
||||
if (this._debuggerConnection) {
|
||||
// Wrapping just to make flow happy :)
|
||||
this._processMessageFromDevice(parsedPayload, this._debuggerConnection);
|
||||
}
|
||||
|
||||
const messageToSend = JSON.stringify(parsedPayload);
|
||||
debuggerSocket.send(messageToSend);
|
||||
}
|
||||
} // Sends single message to device.
|
||||
|
||||
_sendMessageToDevice(message) {
|
||||
try {
|
||||
if (message.event !== "getPages") {
|
||||
debug("(Debugger) (Proxy) -> (Device): " + JSON.stringify(message));
|
||||
}
|
||||
|
||||
this._deviceSocket.send(JSON.stringify(message));
|
||||
} catch (error) {}
|
||||
} // Sends 'getPages' request to device every PAGES_POLLING_INTERVAL milliseconds.
|
||||
|
||||
_setPagesPolling() {
|
||||
setInterval(
|
||||
() =>
|
||||
this._sendMessageToDevice({
|
||||
event: "getPages"
|
||||
}),
|
||||
PAGES_POLLING_INTERVAL
|
||||
);
|
||||
} // We received new React Native Page ID.
|
||||
|
||||
_newReactNativePage(pageId) {
|
||||
debug(`React Native page updated to ${pageId}`);
|
||||
|
||||
if (
|
||||
this._debuggerConnection == null ||
|
||||
this._debuggerConnection.pageId !== REACT_NATIVE_RELOADABLE_PAGE.id
|
||||
) {
|
||||
// We can just remember new page ID without any further actions if no
|
||||
// debugger is currently attached or attached debugger is not
|
||||
// "Reloadable React Native" connection.
|
||||
this._lastReactNativePageId = pageId;
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPageId = this._lastReactNativePageId;
|
||||
this._lastReactNativePageId = pageId;
|
||||
this._isReloading = true; // We already had a debugger connected to React Native page and a
|
||||
// new one appeared - in this case we need to emulate execution context
|
||||
// detroy and resend Debugger.enable and Runtime.enable commands to new
|
||||
// page.
|
||||
|
||||
if (oldPageId != null) {
|
||||
this._sendMessageToDevice({
|
||||
event: "disconnect",
|
||||
payload: {
|
||||
pageId: oldPageId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._sendMessageToDevice({
|
||||
event: "connect",
|
||||
payload: {
|
||||
pageId
|
||||
}
|
||||
});
|
||||
|
||||
const toSend = [
|
||||
{
|
||||
method: "Runtime.enable",
|
||||
id: 1e9
|
||||
},
|
||||
{
|
||||
method: "Debugger.enable",
|
||||
id: 1e9
|
||||
}
|
||||
];
|
||||
|
||||
for (const message of toSend) {
|
||||
this._sendMessageToDevice({
|
||||
event: "wrappedEvent",
|
||||
payload: {
|
||||
pageId: this._getPageId(pageId),
|
||||
wrappedEvent: JSON.stringify(message)
|
||||
}
|
||||
});
|
||||
}
|
||||
} // Allows to make changes in incoming message from device.
|
||||
// eslint-disable-next-line lint/no-unclear-flowtypes
|
||||
|
||||
_processMessageFromDevice(payload, debuggerInfo) {
|
||||
// Replace Android addresses for scriptParsed event.
|
||||
if (payload.method === "Debugger.scriptParsed") {
|
||||
const params = payload.params || {};
|
||||
|
||||
if ("sourceMapURL" in params) {
|
||||
for (let i = 0; i < EMULATOR_LOCALHOST_ADDRESSES.length; ++i) {
|
||||
const address = EMULATOR_LOCALHOST_ADDRESSES[i];
|
||||
|
||||
if (params.sourceMapURL.indexOf(address) >= 0) {
|
||||
payload.params.sourceMapURL = params.sourceMapURL.replace(
|
||||
address,
|
||||
"localhost"
|
||||
);
|
||||
debuggerInfo.originalSourceURLAddress = address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("url" in params) {
|
||||
for (let i = 0; i < EMULATOR_LOCALHOST_ADDRESSES.length; ++i) {
|
||||
const address = EMULATOR_LOCALHOST_ADDRESSES[i];
|
||||
|
||||
if (params.url.indexOf(address) >= 0) {
|
||||
payload.params.url = params.url.replace(address, "localhost");
|
||||
debuggerInfo.originalSourceURLAddress = address;
|
||||
}
|
||||
} // Chrome doesn't download source maps if URL param is not a valid
|
||||
// URL. Some frameworks pass alphanumeric script ID instead of URL which causes
|
||||
// Chrome to not download source maps. In this case we want to prepend script ID
|
||||
// with 'file://' prefix.
|
||||
|
||||
if (payload.params.url.match(/^[0-9a-z]+$/)) {
|
||||
payload.params.url = FILE_PREFIX + payload.params.url;
|
||||
debuggerInfo.prependedFilePrefix = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (debuggerInfo.pageId == REACT_NATIVE_RELOADABLE_PAGE.id) {
|
||||
// Chrome won't use the source map unless it appears to be new.
|
||||
payload.params.sourceMapURL +=
|
||||
"&cachePrevention=" + this._getPageId(debuggerInfo.pageId);
|
||||
payload.params.url +=
|
||||
"&cachePrevention=" + this._getPageId(debuggerInfo.pageId);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
payload.method === "Runtime.executionContextCreated" &&
|
||||
this._isReloading
|
||||
) {
|
||||
// The new context is ready. First notify Chrome that we've reloaded so
|
||||
// it'll resend its breakpoints. If we do this earlier, we may not be
|
||||
// ready to receive them.
|
||||
debuggerInfo.socket.send(
|
||||
JSON.stringify({
|
||||
method: "Runtime.executionContextsCleared"
|
||||
})
|
||||
); // The VM starts in a paused mode. Ask it to resume.
|
||||
// Note that if setting breakpoints in early initialization functions,
|
||||
// there's a currently race condition between these functions executing
|
||||
// and Chrome re-applying the breakpoints due to the message above.
|
||||
//
|
||||
// This is not an issue in VSCode/Nuclide where the IDE knows to resume
|
||||
// at its convenience.
|
||||
|
||||
this._sendMessageToDevice({
|
||||
event: "wrappedEvent",
|
||||
payload: {
|
||||
pageId: this._getPageId(debuggerInfo.pageId),
|
||||
wrappedEvent: JSON.stringify({
|
||||
method: "Debugger.resume",
|
||||
id: 0
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
this._isReloading = false;
|
||||
}
|
||||
} // Allows to make changes in incoming messages from debugger.
|
||||
// eslint-disable-next-line lint/no-unclear-flowtypes
|
||||
|
||||
_processMessageFromDebugger(payload, debuggerInfo) {
|
||||
// If we replaced Android emulator's address to localhost we need to change it back.
|
||||
if (
|
||||
payload.method === "Debugger.setBreakpointByUrl" &&
|
||||
debuggerInfo.originalSourceURLAddress
|
||||
) {
|
||||
const params = payload.params || {};
|
||||
|
||||
if ("url" in params) {
|
||||
payload.params.url = params.url.replace(
|
||||
"localhost",
|
||||
debuggerInfo.originalSourceURLAddress
|
||||
);
|
||||
|
||||
if (
|
||||
payload.params.url.startsWith(FILE_PREFIX) &&
|
||||
debuggerInfo.prependedFilePrefix
|
||||
) {
|
||||
// Remove fake URL prefix if we modified URL in _processMessageFromDevice.
|
||||
payload.params.url = payload.params.url.slice(FILE_PREFIX.length);
|
||||
}
|
||||
}
|
||||
|
||||
if ("urlRegex" in params) {
|
||||
payload.params.urlRegex = params.urlRegex.replace(
|
||||
/localhost/g,
|
||||
debuggerInfo.originalSourceURLAddress
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getPageId(pageId) {
|
||||
if (
|
||||
pageId === REACT_NATIVE_RELOADABLE_PAGE.id &&
|
||||
this._lastReactNativePageId != null
|
||||
) {
|
||||
return this._lastReactNativePageId;
|
||||
} else {
|
||||
return pageId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Device;
|
441
node_modules/metro-inspector-proxy/src/Device.js.flow
generated
vendored
Normal file
441
node_modules/metro-inspector-proxy/src/Device.js.flow
generated
vendored
Normal file
@ -0,0 +1,441 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {Page, MessageFromDevice, MessageToDevice} from './types';
|
||||
import WS from 'ws';
|
||||
|
||||
const PAGES_POLLING_INTERVAL = 1000;
|
||||
|
||||
const debug = require('debug')('Metro:InspectorProxy');
|
||||
|
||||
// Android's stock emulator and other emulators such as genymotion use a standard localhost alias.
|
||||
const EMULATOR_LOCALHOST_ADDRESSES: Array<string> = ['10.0.2.2', '10.0.3.2'];
|
||||
|
||||
// Prefix for script URLs that are alphanumeric IDs. See comment in _processMessageFromDevice method for
|
||||
// more details.
|
||||
const FILE_PREFIX = 'file://';
|
||||
|
||||
type DebuggerInfo = {
|
||||
// Debugger web socket connection
|
||||
socket: WS,
|
||||
// If we replaced address (like '10.0.2.2') to localhost we need to store original
|
||||
// address because Chrome uses URL or urlRegex params (instead of scriptId) to set breakpoints.
|
||||
originalSourceURLAddress?: string,
|
||||
prependedFilePrefix: boolean,
|
||||
pageId: string,
|
||||
...
|
||||
};
|
||||
|
||||
const REACT_NATIVE_RELOADABLE_PAGE = {
|
||||
id: '-1',
|
||||
title: 'React Native Experimental (Improved Chrome Reloads)',
|
||||
vm: "don't use",
|
||||
app: "don't use",
|
||||
};
|
||||
|
||||
/**
|
||||
* Device class represents single device connection to Inspector Proxy. Each device
|
||||
* can have multiple inspectable pages.
|
||||
*/
|
||||
class Device {
|
||||
// ID of the device.
|
||||
_id: number;
|
||||
|
||||
// Name of the device.
|
||||
_name: string;
|
||||
|
||||
// Package name of the app.
|
||||
_app: string;
|
||||
|
||||
// Stores socket connection between Inspector Proxy and device.
|
||||
_deviceSocket: WS;
|
||||
|
||||
// Stores last list of device's pages.
|
||||
_pages: Array<Page>;
|
||||
|
||||
// Stores information about currently connected debugger (if any).
|
||||
_debuggerConnection: ?DebuggerInfo = null;
|
||||
|
||||
// Last known Page ID of the React Native page.
|
||||
// This is used by debugger connections that don't have PageID specified
|
||||
// (and will interact with the latest React Native page).
|
||||
_lastReactNativePageId: ?string = null;
|
||||
|
||||
// Whether we are in the middle of a reload in the REACT_NATIVE_RELOADABLE_PAGE.
|
||||
_isReloading: boolean = false;
|
||||
|
||||
// The previous "GetPages" message, for deduplication in debug logs.
|
||||
_lastGetPagesMessage: string = '';
|
||||
|
||||
constructor(id: number, name: string, app: string, socket: WS) {
|
||||
this._id = id;
|
||||
this._name = name;
|
||||
this._app = app;
|
||||
this._pages = [];
|
||||
this._deviceSocket = socket;
|
||||
|
||||
this._deviceSocket.on('message', (message: string) => {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
if (parsedMessage.event === 'getPages') {
|
||||
// There's a 'getPages' message every second, so only show them if they change
|
||||
if (message !== this._lastGetPagesMessage) {
|
||||
debug(
|
||||
'(Debugger) (Proxy) <- (Device), getPages ping has changed: ' +
|
||||
message,
|
||||
);
|
||||
this._lastGetPagesMessage = message;
|
||||
}
|
||||
} else {
|
||||
debug('(Debugger) (Proxy) <- (Device): ' + message);
|
||||
}
|
||||
this._handleMessageFromDevice(parsedMessage);
|
||||
});
|
||||
this._deviceSocket.on('close', () => {
|
||||
// Device disconnected - close debugger connection.
|
||||
if (this._debuggerConnection) {
|
||||
this._debuggerConnection.socket.close();
|
||||
this._debuggerConnection = null;
|
||||
}
|
||||
});
|
||||
|
||||
this._setPagesPolling();
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
getPagesList(): Array<Page> {
|
||||
if (this._lastReactNativePageId) {
|
||||
return this._pages.concat(REACT_NATIVE_RELOADABLE_PAGE);
|
||||
} else {
|
||||
return this._pages;
|
||||
}
|
||||
}
|
||||
|
||||
// Handles new debugger connection to this device:
|
||||
// 1. Sends connect event to device
|
||||
// 2. Forwards all messages from the debugger to device as wrappedEvent
|
||||
// 3. Sends disconnect event to device when debugger connection socket closes.
|
||||
handleDebuggerConnection(socket: WS, pageId: string) {
|
||||
// Disconnect current debugger if we already have debugger connected.
|
||||
if (this._debuggerConnection) {
|
||||
this._debuggerConnection.socket.close();
|
||||
this._debuggerConnection = null;
|
||||
}
|
||||
|
||||
const debuggerInfo = {
|
||||
socket,
|
||||
prependedFilePrefix: false,
|
||||
pageId,
|
||||
};
|
||||
this._debuggerConnection = debuggerInfo;
|
||||
|
||||
debug(`Got new debugger connection for page ${pageId} of ${this._name}`);
|
||||
|
||||
this._sendMessageToDevice({
|
||||
event: 'connect',
|
||||
payload: {
|
||||
pageId: this._getPageId(pageId),
|
||||
},
|
||||
});
|
||||
|
||||
socket.on('message', (message: string) => {
|
||||
debug('(Debugger) -> (Proxy) (Device): ' + message);
|
||||
const parsedMessage = JSON.parse(message);
|
||||
this._processMessageFromDebugger(parsedMessage, debuggerInfo);
|
||||
|
||||
this._sendMessageToDevice({
|
||||
event: 'wrappedEvent',
|
||||
payload: {
|
||||
pageId: this._getPageId(pageId),
|
||||
wrappedEvent: JSON.stringify(parsedMessage),
|
||||
},
|
||||
});
|
||||
});
|
||||
socket.on('close', () => {
|
||||
debug(`Debugger for page ${pageId} and ${this._name} disconnected.`);
|
||||
this._sendMessageToDevice({
|
||||
event: 'disconnect',
|
||||
payload: {
|
||||
pageId: this._getPageId(pageId),
|
||||
},
|
||||
});
|
||||
this._debuggerConnection = null;
|
||||
});
|
||||
|
||||
const sendFunc = socket.send;
|
||||
socket.send = function(message: string) {
|
||||
debug('(Debugger) <- (Proxy) (Device): ' + message);
|
||||
return sendFunc.call(socket, message);
|
||||
};
|
||||
}
|
||||
|
||||
// Handles messages received from device:
|
||||
// 1. For getPages responses updates local _pages list.
|
||||
// 2. All other messages are forwarded to debugger as wrappedEvent.
|
||||
//
|
||||
// In the future more logic will be added to this method for modifying
|
||||
// some of the messages (like updating messages with source maps and file
|
||||
// locations).
|
||||
_handleMessageFromDevice(message: MessageFromDevice) {
|
||||
if (message.event === 'getPages') {
|
||||
this._pages = message.payload;
|
||||
|
||||
// Check if device have new React Native page.
|
||||
// There is usually no more than 2-3 pages per device so this operation
|
||||
// is not expensive.
|
||||
// TODO(hypuk): It is better for VM to send update event when new page is
|
||||
// created instead of manually checking this on every getPages result.
|
||||
for (let i = 0; i < this._pages.length; ++i) {
|
||||
if (this._pages[i].title.indexOf('React') >= 0) {
|
||||
if (this._pages[i].id != this._lastReactNativePageId) {
|
||||
this._newReactNativePage(this._pages[i].id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (message.event === 'disconnect') {
|
||||
// Device sends disconnect events only when page is reloaded or
|
||||
// if debugger socket was disconnected.
|
||||
const pageId = message.payload.pageId;
|
||||
const debuggerSocket = this._debuggerConnection
|
||||
? this._debuggerConnection.socket
|
||||
: null;
|
||||
if (debuggerSocket && debuggerSocket.readyState === WS.OPEN) {
|
||||
if (
|
||||
this._debuggerConnection != null &&
|
||||
this._debuggerConnection.pageId !== REACT_NATIVE_RELOADABLE_PAGE.id
|
||||
) {
|
||||
debug(`Page ${pageId} is reloading.`);
|
||||
debuggerSocket.send(JSON.stringify({method: 'reload'}));
|
||||
}
|
||||
}
|
||||
} else if (message.event === 'wrappedEvent') {
|
||||
if (this._debuggerConnection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: Is it possible that we received message for pageID that does not
|
||||
// correspond to current debugger connection?
|
||||
|
||||
const debuggerSocket = this._debuggerConnection.socket;
|
||||
if (debuggerSocket == null || debuggerSocket.readyState !== WS.OPEN) {
|
||||
// TODO(hypuk): Send error back to device?
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedPayload = JSON.parse(message.payload.wrappedEvent);
|
||||
|
||||
if (this._debuggerConnection) {
|
||||
// Wrapping just to make flow happy :)
|
||||
this._processMessageFromDevice(parsedPayload, this._debuggerConnection);
|
||||
}
|
||||
|
||||
const messageToSend = JSON.stringify(parsedPayload);
|
||||
debuggerSocket.send(messageToSend);
|
||||
}
|
||||
}
|
||||
|
||||
// Sends single message to device.
|
||||
_sendMessageToDevice(message: MessageToDevice) {
|
||||
try {
|
||||
if (message.event !== 'getPages') {
|
||||
debug('(Debugger) (Proxy) -> (Device): ' + JSON.stringify(message));
|
||||
}
|
||||
this._deviceSocket.send(JSON.stringify(message));
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
// Sends 'getPages' request to device every PAGES_POLLING_INTERVAL milliseconds.
|
||||
_setPagesPolling() {
|
||||
setInterval(
|
||||
() => this._sendMessageToDevice({event: 'getPages'}),
|
||||
PAGES_POLLING_INTERVAL,
|
||||
);
|
||||
}
|
||||
|
||||
// We received new React Native Page ID.
|
||||
_newReactNativePage(pageId: string) {
|
||||
debug(`React Native page updated to ${pageId}`);
|
||||
if (
|
||||
this._debuggerConnection == null ||
|
||||
this._debuggerConnection.pageId !== REACT_NATIVE_RELOADABLE_PAGE.id
|
||||
) {
|
||||
// We can just remember new page ID without any further actions if no
|
||||
// debugger is currently attached or attached debugger is not
|
||||
// "Reloadable React Native" connection.
|
||||
this._lastReactNativePageId = pageId;
|
||||
return;
|
||||
}
|
||||
const oldPageId = this._lastReactNativePageId;
|
||||
this._lastReactNativePageId = pageId;
|
||||
this._isReloading = true;
|
||||
|
||||
// We already had a debugger connected to React Native page and a
|
||||
// new one appeared - in this case we need to emulate execution context
|
||||
// detroy and resend Debugger.enable and Runtime.enable commands to new
|
||||
// page.
|
||||
|
||||
if (oldPageId != null) {
|
||||
this._sendMessageToDevice({
|
||||
event: 'disconnect',
|
||||
payload: {
|
||||
pageId: oldPageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this._sendMessageToDevice({
|
||||
event: 'connect',
|
||||
payload: {
|
||||
pageId,
|
||||
},
|
||||
});
|
||||
|
||||
const toSend = [
|
||||
{method: 'Runtime.enable', id: 1e9},
|
||||
{method: 'Debugger.enable', id: 1e9},
|
||||
];
|
||||
|
||||
for (const message of toSend) {
|
||||
this._sendMessageToDevice({
|
||||
event: 'wrappedEvent',
|
||||
payload: {
|
||||
pageId: this._getPageId(pageId),
|
||||
wrappedEvent: JSON.stringify(message),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Allows to make changes in incoming message from device.
|
||||
// eslint-disable-next-line lint/no-unclear-flowtypes
|
||||
_processMessageFromDevice(payload: Object, debuggerInfo: DebuggerInfo) {
|
||||
// Replace Android addresses for scriptParsed event.
|
||||
if (payload.method === 'Debugger.scriptParsed') {
|
||||
const params = payload.params || {};
|
||||
if ('sourceMapURL' in params) {
|
||||
for (let i = 0; i < EMULATOR_LOCALHOST_ADDRESSES.length; ++i) {
|
||||
const address = EMULATOR_LOCALHOST_ADDRESSES[i];
|
||||
if (params.sourceMapURL.indexOf(address) >= 0) {
|
||||
payload.params.sourceMapURL = params.sourceMapURL.replace(
|
||||
address,
|
||||
'localhost',
|
||||
);
|
||||
debuggerInfo.originalSourceURLAddress = address;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ('url' in params) {
|
||||
for (let i = 0; i < EMULATOR_LOCALHOST_ADDRESSES.length; ++i) {
|
||||
const address = EMULATOR_LOCALHOST_ADDRESSES[i];
|
||||
if (params.url.indexOf(address) >= 0) {
|
||||
payload.params.url = params.url.replace(address, 'localhost');
|
||||
debuggerInfo.originalSourceURLAddress = address;
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome doesn't download source maps if URL param is not a valid
|
||||
// URL. Some frameworks pass alphanumeric script ID instead of URL which causes
|
||||
// Chrome to not download source maps. In this case we want to prepend script ID
|
||||
// with 'file://' prefix.
|
||||
if (payload.params.url.match(/^[0-9a-z]+$/)) {
|
||||
payload.params.url = FILE_PREFIX + payload.params.url;
|
||||
debuggerInfo.prependedFilePrefix = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (debuggerInfo.pageId == REACT_NATIVE_RELOADABLE_PAGE.id) {
|
||||
// Chrome won't use the source map unless it appears to be new.
|
||||
payload.params.sourceMapURL +=
|
||||
'&cachePrevention=' + this._getPageId(debuggerInfo.pageId);
|
||||
payload.params.url +=
|
||||
'&cachePrevention=' + this._getPageId(debuggerInfo.pageId);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
payload.method === 'Runtime.executionContextCreated' &&
|
||||
this._isReloading
|
||||
) {
|
||||
// The new context is ready. First notify Chrome that we've reloaded so
|
||||
// it'll resend its breakpoints. If we do this earlier, we may not be
|
||||
// ready to receive them.
|
||||
debuggerInfo.socket.send(
|
||||
JSON.stringify({method: 'Runtime.executionContextsCleared'}),
|
||||
);
|
||||
|
||||
// The VM starts in a paused mode. Ask it to resume.
|
||||
// Note that if setting breakpoints in early initialization functions,
|
||||
// there's a currently race condition between these functions executing
|
||||
// and Chrome re-applying the breakpoints due to the message above.
|
||||
//
|
||||
// This is not an issue in VSCode/Nuclide where the IDE knows to resume
|
||||
// at its convenience.
|
||||
this._sendMessageToDevice({
|
||||
event: 'wrappedEvent',
|
||||
payload: {
|
||||
pageId: this._getPageId(debuggerInfo.pageId),
|
||||
wrappedEvent: JSON.stringify({method: 'Debugger.resume', id: 0}),
|
||||
},
|
||||
});
|
||||
|
||||
this._isReloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Allows to make changes in incoming messages from debugger.
|
||||
// eslint-disable-next-line lint/no-unclear-flowtypes
|
||||
_processMessageFromDebugger(payload: Object, debuggerInfo: DebuggerInfo) {
|
||||
// If we replaced Android emulator's address to localhost we need to change it back.
|
||||
if (
|
||||
payload.method === 'Debugger.setBreakpointByUrl' &&
|
||||
debuggerInfo.originalSourceURLAddress
|
||||
) {
|
||||
const params = payload.params || {};
|
||||
if ('url' in params) {
|
||||
payload.params.url = params.url.replace(
|
||||
'localhost',
|
||||
debuggerInfo.originalSourceURLAddress,
|
||||
);
|
||||
|
||||
if (
|
||||
payload.params.url.startsWith(FILE_PREFIX) &&
|
||||
debuggerInfo.prependedFilePrefix
|
||||
) {
|
||||
// Remove fake URL prefix if we modified URL in _processMessageFromDevice.
|
||||
payload.params.url = payload.params.url.slice(FILE_PREFIX.length);
|
||||
}
|
||||
}
|
||||
if ('urlRegex' in params) {
|
||||
payload.params.urlRegex = params.urlRegex.replace(
|
||||
/localhost/g,
|
||||
debuggerInfo.originalSourceURLAddress,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getPageId(pageId: string): string {
|
||||
if (
|
||||
pageId === REACT_NATIVE_RELOADABLE_PAGE.id &&
|
||||
this._lastReactNativePageId != null
|
||||
) {
|
||||
return this._lastReactNativePageId;
|
||||
} else {
|
||||
return pageId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Device;
|
310
node_modules/metro-inspector-proxy/src/InspectorProxy.js
generated
vendored
Normal file
310
node_modules/metro-inspector-proxy/src/InspectorProxy.js
generated
vendored
Normal file
@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
|
||||
try {
|
||||
var info = gen[key](arg);
|
||||
var value = info.value;
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
if (info.done) {
|
||||
resolve(value);
|
||||
} else {
|
||||
Promise.resolve(value).then(_next, _throw);
|
||||
}
|
||||
}
|
||||
|
||||
function _asyncToGenerator(fn) {
|
||||
return function() {
|
||||
var self = this,
|
||||
args = arguments;
|
||||
return new Promise(function(resolve, reject) {
|
||||
var gen = fn.apply(self, args);
|
||||
function _next(value) {
|
||||
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
|
||||
}
|
||||
function _throw(err) {
|
||||
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
|
||||
}
|
||||
_next(undefined);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function _slicedToArray(arr, i) {
|
||||
return (
|
||||
_arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest()
|
||||
);
|
||||
}
|
||||
|
||||
function _nonIterableRest() {
|
||||
throw new TypeError("Invalid attempt to destructure non-iterable instance");
|
||||
}
|
||||
|
||||
function _iterableToArrayLimit(arr, i) {
|
||||
var _arr = [];
|
||||
var _n = true;
|
||||
var _d = false;
|
||||
var _e = undefined;
|
||||
try {
|
||||
for (
|
||||
var _i = arr[Symbol.iterator](), _s;
|
||||
!(_n = (_s = _i.next()).done);
|
||||
_n = true
|
||||
) {
|
||||
_arr.push(_s.value);
|
||||
if (i && _arr.length === i) break;
|
||||
}
|
||||
} catch (err) {
|
||||
_d = true;
|
||||
_e = err;
|
||||
} finally {
|
||||
try {
|
||||
if (!_n && _i["return"] != null) _i["return"]();
|
||||
} finally {
|
||||
if (_d) throw _e;
|
||||
}
|
||||
}
|
||||
return _arr;
|
||||
}
|
||||
|
||||
function _arrayWithHoles(arr) {
|
||||
if (Array.isArray(arr)) return arr;
|
||||
}
|
||||
|
||||
function _defineProperty(obj, key, value) {
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
const Device = require("./Device");
|
||||
|
||||
const WS = require("ws");
|
||||
|
||||
const debug = require("debug")("Metro:InspectorProxy");
|
||||
|
||||
const url = require("url");
|
||||
|
||||
const WS_DEVICE_URL = "/inspector/device";
|
||||
const WS_DEBUGGER_URL = "/inspector/debug";
|
||||
const PAGES_LIST_JSON_URL = "/json";
|
||||
const PAGES_LIST_JSON_URL_2 = "/json/list";
|
||||
const PAGES_LIST_JSON_VERSION_URL = "/json/version";
|
||||
const INTERNAL_ERROR_CODE = 1011;
|
||||
/**
|
||||
* Main Inspector Proxy class that connects JavaScript VM inside Android/iOS apps and JS debugger.
|
||||
*/
|
||||
|
||||
class InspectorProxy {
|
||||
// Maps device ID to Device instance.
|
||||
// Internal counter for device IDs -- just gets incremented for each new device.
|
||||
// We store server's address with port (like '127.0.0.1:8081') to be able to build URLs
|
||||
// (devtoolsFrontendUrl and webSocketDebuggerUrl) for page descriptions. These URLs are used
|
||||
// by debugger to know where to connect.
|
||||
constructor() {
|
||||
_defineProperty(this, "_deviceCounter", 0);
|
||||
|
||||
_defineProperty(this, "_serverAddressWithPort", "");
|
||||
|
||||
this._devices = new Map();
|
||||
} // Process HTTP request sent to server. We only respond to 2 HTTP requests:
|
||||
// 1. /json/version returns Chrome debugger protocol version that we use
|
||||
// 2. /json and /json/list returns list of page descriptions (list of inspectable apps).
|
||||
// This list is combined from all the connected devices.
|
||||
|
||||
processRequest(request, response, next) {
|
||||
if (
|
||||
request.url === PAGES_LIST_JSON_URL ||
|
||||
request.url === PAGES_LIST_JSON_URL_2
|
||||
) {
|
||||
// Build list of pages from all devices.
|
||||
let result = [];
|
||||
Array.from(this._devices.entries()).forEach(_ref => {
|
||||
let _ref2 = _slicedToArray(_ref, 2),
|
||||
deviceId = _ref2[0],
|
||||
device = _ref2[1];
|
||||
|
||||
result = result.concat(
|
||||
device
|
||||
.getPagesList()
|
||||
.map(page => this._buildPageDescription(deviceId, device, page))
|
||||
);
|
||||
});
|
||||
|
||||
this._sendJsonResponse(response, result);
|
||||
} else if (request.url === PAGES_LIST_JSON_VERSION_URL) {
|
||||
this._sendJsonResponse(response, {
|
||||
Browser: "Mobile JavaScript",
|
||||
"Protocol-Version": "1.1"
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} // Adds websocket listeners to the provided HTTP/HTTPS server.
|
||||
|
||||
addWebSocketListener(server) {
|
||||
const _server$address = server.address(),
|
||||
port = _server$address.port;
|
||||
|
||||
if (server.address().family === "IPv6") {
|
||||
this._serverAddressWithPort = `[::]:${port}`;
|
||||
} else {
|
||||
this._serverAddressWithPort = `localhost:${port}`;
|
||||
}
|
||||
|
||||
this._addDeviceConnectionHandler(server);
|
||||
|
||||
this._addDebuggerConnectionHandler(server);
|
||||
} // Converts page information received from device into PageDescription object
|
||||
// that is sent to debugger.
|
||||
|
||||
_buildPageDescription(deviceId, device, page) {
|
||||
const debuggerUrl = `${
|
||||
this._serverAddressWithPort
|
||||
}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`;
|
||||
const webSocketDebuggerUrl = "ws://" + debuggerUrl;
|
||||
const devtoolsFrontendUrl =
|
||||
"chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=" +
|
||||
encodeURIComponent(debuggerUrl);
|
||||
return {
|
||||
id: `${deviceId}-${page.id}`,
|
||||
description: page.app,
|
||||
title: page.title,
|
||||
faviconUrl: "https://reactjs.org/favicon.ico",
|
||||
devtoolsFrontendUrl,
|
||||
type: "node",
|
||||
webSocketDebuggerUrl,
|
||||
vm: page.vm
|
||||
};
|
||||
} // Sends object as response to HTTP request.
|
||||
// Just serializes object using JSON and sets required headers.
|
||||
|
||||
_sendJsonResponse(response, object) {
|
||||
const data = JSON.stringify(object, null, 2);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"Cache-Control": "no-cache",
|
||||
"Content-Length": data.length.toString(),
|
||||
Connection: "close"
|
||||
});
|
||||
response.end(data);
|
||||
} // Adds websocket handler for device connections.
|
||||
// Device connects to /inspector/device and passes device and app names as
|
||||
// HTTP GET params.
|
||||
// For each new websocket connection we parse device and app names and create
|
||||
// new instance of Device class.
|
||||
|
||||
_addDeviceConnectionHandler(server) {
|
||||
var _this = this;
|
||||
|
||||
const wss = new WS.Server({
|
||||
server,
|
||||
path: WS_DEVICE_URL,
|
||||
perMessageDeflate: true
|
||||
});
|
||||
wss.on(
|
||||
"connection",
|
||||
/*#__PURE__*/
|
||||
(function() {
|
||||
var _ref3 = _asyncToGenerator(function*(socket) {
|
||||
try {
|
||||
const query =
|
||||
url.parse(socket.upgradeReq.url || "", true).query || {};
|
||||
const deviceName = query.name || "Unknown";
|
||||
const appName = query.app || "Unknown";
|
||||
const deviceId = _this._deviceCounter++;
|
||||
|
||||
_this._devices.set(
|
||||
deviceId,
|
||||
new Device(deviceId, deviceName, appName, socket)
|
||||
);
|
||||
|
||||
debug(`Got new connection: device=${deviceName}, app=${appName}`);
|
||||
socket.on("close", () => {
|
||||
_this._devices.delete(deviceId);
|
||||
|
||||
debug(`Device ${deviceName} disconnected.`);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("error", e);
|
||||
socket.close(INTERNAL_ERROR_CODE, e);
|
||||
}
|
||||
});
|
||||
|
||||
return function(_x) {
|
||||
return _ref3.apply(this, arguments);
|
||||
};
|
||||
})()
|
||||
);
|
||||
} // Adds websocket handler for debugger connections.
|
||||
// Debugger connects to webSocketDebuggerUrl that we return as part of page description
|
||||
// in /json response.
|
||||
// When debugger connects we try to parse device and page IDs from the query and pass
|
||||
// websocket object to corresponding Device instance.
|
||||
|
||||
_addDebuggerConnectionHandler(server) {
|
||||
var _this2 = this;
|
||||
|
||||
const wss = new WS.Server({
|
||||
server,
|
||||
path: WS_DEBUGGER_URL,
|
||||
perMessageDeflate: false
|
||||
});
|
||||
wss.on(
|
||||
"connection",
|
||||
/*#__PURE__*/
|
||||
(function() {
|
||||
var _ref4 = _asyncToGenerator(function*(socket) {
|
||||
try {
|
||||
const query =
|
||||
url.parse(socket.upgradeReq.url || "", true).query || {};
|
||||
const deviceId = query.device;
|
||||
const pageId = query.page;
|
||||
|
||||
if (deviceId == null || pageId == null) {
|
||||
throw new Error(
|
||||
"Incorrect URL - must provide device and page IDs"
|
||||
);
|
||||
}
|
||||
|
||||
const device = _this2._devices.get(parseInt(deviceId, 10));
|
||||
|
||||
if (device == null) {
|
||||
throw new Error("Unknown device with ID " + deviceId);
|
||||
}
|
||||
|
||||
device.handleDebuggerConnection(socket, pageId);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
socket.close(INTERNAL_ERROR_CODE, e);
|
||||
}
|
||||
});
|
||||
|
||||
return function(_x2) {
|
||||
return _ref4.apply(this, arguments);
|
||||
};
|
||||
})()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InspectorProxy;
|
217
node_modules/metro-inspector-proxy/src/InspectorProxy.js.flow
generated
vendored
Normal file
217
node_modules/metro-inspector-proxy/src/InspectorProxy.js.flow
generated
vendored
Normal file
@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Device = require('./Device');
|
||||
const WS = require('ws');
|
||||
|
||||
const debug = require('debug')('Metro:InspectorProxy');
|
||||
const url = require('url');
|
||||
|
||||
import type {
|
||||
JsonPagesListResponse,
|
||||
JsonVersionResponse,
|
||||
Page,
|
||||
PageDescription,
|
||||
} from './types';
|
||||
import type {Server as HttpServer, IncomingMessage, ServerResponse} from 'http';
|
||||
import type {Server as HttpsServer} from 'https';
|
||||
|
||||
const WS_DEVICE_URL = '/inspector/device';
|
||||
const WS_DEBUGGER_URL = '/inspector/debug';
|
||||
const PAGES_LIST_JSON_URL = '/json';
|
||||
const PAGES_LIST_JSON_URL_2 = '/json/list';
|
||||
const PAGES_LIST_JSON_VERSION_URL = '/json/version';
|
||||
|
||||
const INTERNAL_ERROR_CODE = 1011;
|
||||
|
||||
/**
|
||||
* Main Inspector Proxy class that connects JavaScript VM inside Android/iOS apps and JS debugger.
|
||||
*/
|
||||
class InspectorProxy {
|
||||
// Maps device ID to Device instance.
|
||||
_devices: Map<number, Device>;
|
||||
|
||||
// Internal counter for device IDs -- just gets incremented for each new device.
|
||||
_deviceCounter: number = 0;
|
||||
|
||||
// We store server's address with port (like '127.0.0.1:8081') to be able to build URLs
|
||||
// (devtoolsFrontendUrl and webSocketDebuggerUrl) for page descriptions. These URLs are used
|
||||
// by debugger to know where to connect.
|
||||
_serverAddressWithPort: string = '';
|
||||
|
||||
constructor() {
|
||||
this._devices = new Map();
|
||||
}
|
||||
|
||||
// Process HTTP request sent to server. We only respond to 2 HTTP requests:
|
||||
// 1. /json/version returns Chrome debugger protocol version that we use
|
||||
// 2. /json and /json/list returns list of page descriptions (list of inspectable apps).
|
||||
// This list is combined from all the connected devices.
|
||||
processRequest(
|
||||
request: IncomingMessage,
|
||||
response: ServerResponse,
|
||||
next: (?Error) => mixed,
|
||||
) {
|
||||
if (
|
||||
request.url === PAGES_LIST_JSON_URL ||
|
||||
request.url === PAGES_LIST_JSON_URL_2
|
||||
) {
|
||||
// Build list of pages from all devices.
|
||||
let result = [];
|
||||
Array.from(this._devices.entries()).forEach(
|
||||
([deviceId: number, device: Device]) => {
|
||||
result = result.concat(
|
||||
device
|
||||
.getPagesList()
|
||||
.map((page: Page) =>
|
||||
this._buildPageDescription(deviceId, device, page),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
this._sendJsonResponse(response, result);
|
||||
} else if (request.url === PAGES_LIST_JSON_VERSION_URL) {
|
||||
this._sendJsonResponse(response, {
|
||||
Browser: 'Mobile JavaScript',
|
||||
'Protocol-Version': '1.1',
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
// Adds websocket listeners to the provided HTTP/HTTPS server.
|
||||
addWebSocketListener(server: HttpServer | HttpsServer) {
|
||||
const {port} = server.address();
|
||||
if (server.address().family === 'IPv6') {
|
||||
this._serverAddressWithPort = `[::]:${port}`;
|
||||
} else {
|
||||
this._serverAddressWithPort = `localhost:${port}`;
|
||||
}
|
||||
this._addDeviceConnectionHandler(server);
|
||||
this._addDebuggerConnectionHandler(server);
|
||||
}
|
||||
|
||||
// Converts page information received from device into PageDescription object
|
||||
// that is sent to debugger.
|
||||
_buildPageDescription(
|
||||
deviceId: number,
|
||||
device: Device,
|
||||
page: Page,
|
||||
): PageDescription {
|
||||
const debuggerUrl = `${
|
||||
this._serverAddressWithPort
|
||||
}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`;
|
||||
const webSocketDebuggerUrl = 'ws://' + debuggerUrl;
|
||||
const devtoolsFrontendUrl =
|
||||
'chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=' +
|
||||
encodeURIComponent(debuggerUrl);
|
||||
return {
|
||||
id: `${deviceId}-${page.id}`,
|
||||
description: page.app,
|
||||
title: page.title,
|
||||
faviconUrl: 'https://reactjs.org/favicon.ico',
|
||||
devtoolsFrontendUrl,
|
||||
type: 'node',
|
||||
webSocketDebuggerUrl,
|
||||
vm: page.vm,
|
||||
};
|
||||
}
|
||||
|
||||
// Sends object as response to HTTP request.
|
||||
// Just serializes object using JSON and sets required headers.
|
||||
_sendJsonResponse(
|
||||
response: ServerResponse,
|
||||
object: JsonPagesListResponse | JsonVersionResponse,
|
||||
) {
|
||||
const data = JSON.stringify(object, null, 2);
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Length': data.length.toString(),
|
||||
Connection: 'close',
|
||||
});
|
||||
response.end(data);
|
||||
}
|
||||
|
||||
// Adds websocket handler for device connections.
|
||||
// Device connects to /inspector/device and passes device and app names as
|
||||
// HTTP GET params.
|
||||
// For each new websocket connection we parse device and app names and create
|
||||
// new instance of Device class.
|
||||
_addDeviceConnectionHandler(server: HttpServer | HttpsServer) {
|
||||
const wss = new WS.Server({
|
||||
server,
|
||||
path: WS_DEVICE_URL,
|
||||
perMessageDeflate: true,
|
||||
});
|
||||
wss.on('connection', async (socket: WS) => {
|
||||
try {
|
||||
const query = url.parse(socket.upgradeReq.url || '', true).query || {};
|
||||
const deviceName = query.name || 'Unknown';
|
||||
const appName = query.app || 'Unknown';
|
||||
const deviceId = this._deviceCounter++;
|
||||
this._devices.set(
|
||||
deviceId,
|
||||
new Device(deviceId, deviceName, appName, socket),
|
||||
);
|
||||
|
||||
debug(`Got new connection: device=${deviceName}, app=${appName}`);
|
||||
|
||||
socket.on('close', () => {
|
||||
this._devices.delete(deviceId);
|
||||
debug(`Device ${deviceName} disconnected.`);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('error', e);
|
||||
socket.close(INTERNAL_ERROR_CODE, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Adds websocket handler for debugger connections.
|
||||
// Debugger connects to webSocketDebuggerUrl that we return as part of page description
|
||||
// in /json response.
|
||||
// When debugger connects we try to parse device and page IDs from the query and pass
|
||||
// websocket object to corresponding Device instance.
|
||||
_addDebuggerConnectionHandler(server: HttpServer | HttpsServer) {
|
||||
const wss = new WS.Server({
|
||||
server,
|
||||
path: WS_DEBUGGER_URL,
|
||||
perMessageDeflate: false,
|
||||
});
|
||||
wss.on('connection', async (socket: WS) => {
|
||||
try {
|
||||
const query = url.parse(socket.upgradeReq.url || '', true).query || {};
|
||||
const deviceId = query.device;
|
||||
const pageId = query.page;
|
||||
|
||||
if (deviceId == null || pageId == null) {
|
||||
throw new Error('Incorrect URL - must provide device and page IDs');
|
||||
}
|
||||
|
||||
const device = this._devices.get(parseInt(deviceId, 10));
|
||||
if (device == null) {
|
||||
throw new Error('Unknown device with ID ' + deviceId);
|
||||
}
|
||||
|
||||
device.handleDebuggerConnection(socket, pageId);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
socket.close(INTERNAL_ERROR_CODE, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InspectorProxy;
|
23
node_modules/metro-inspector-proxy/src/cli.js
generated
vendored
Executable file
23
node_modules/metro-inspector-proxy/src/cli.js
generated
vendored
Executable file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const yargs = require("yargs");
|
||||
|
||||
const _require = require("./index"),
|
||||
runInspectorProxy = _require.runInspectorProxy;
|
||||
|
||||
yargs.option("port", {
|
||||
alias: "p",
|
||||
describe: "port to run inspector proxy on",
|
||||
type: "number",
|
||||
default: 8081
|
||||
});
|
||||
runInspectorProxy(yargs.argv.port);
|
24
node_modules/metro-inspector-proxy/src/cli.js.flow
generated
vendored
Normal file
24
node_modules/metro-inspector-proxy/src/cli.js.flow
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const yargs = require('yargs');
|
||||
|
||||
const {runInspectorProxy} = require('./index');
|
||||
|
||||
yargs.option('port', {
|
||||
alias: 'p',
|
||||
describe: 'port to run inspector proxy on',
|
||||
type: 'number',
|
||||
default: 8081,
|
||||
});
|
||||
|
||||
runInspectorProxy((yargs.argv.port: any));
|
34
node_modules/metro-inspector-proxy/src/index.js
generated
vendored
Normal file
34
node_modules/metro-inspector-proxy/src/index.js
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const InspectorProxy = require("./InspectorProxy"); // Runs new HTTP Server and attaches Inspector Proxy to it.
|
||||
// Requires are inlined here because we don't want to import them
|
||||
// when someone needs only InspectorProxy instance (without starting
|
||||
// new HTTP server).
|
||||
|
||||
function runInspectorProxy(port) {
|
||||
const inspectorProxy = new InspectorProxy();
|
||||
|
||||
const app = require("connect")();
|
||||
|
||||
app.use(inspectorProxy.processRequest.bind(inspectorProxy));
|
||||
|
||||
const httpServer = require("http").createServer(app);
|
||||
|
||||
httpServer.listen(port, "127.0.0.1", () => {
|
||||
inspectorProxy.addWebSocketListener(httpServer);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
InspectorProxy,
|
||||
runInspectorProxy
|
||||
};
|
30
node_modules/metro-inspector-proxy/src/index.js.flow
generated
vendored
Normal file
30
node_modules/metro-inspector-proxy/src/index.js.flow
generated
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const InspectorProxy = require('./InspectorProxy');
|
||||
|
||||
// Runs new HTTP Server and attaches Inspector Proxy to it.
|
||||
// Requires are inlined here because we don't want to import them
|
||||
// when someone needs only InspectorProxy instance (without starting
|
||||
// new HTTP server).
|
||||
function runInspectorProxy(port: number) {
|
||||
const inspectorProxy = new InspectorProxy();
|
||||
const app = require('connect')();
|
||||
app.use(inspectorProxy.processRequest.bind(inspectorProxy));
|
||||
|
||||
const httpServer = require('http').createServer(app);
|
||||
httpServer.listen(port, '127.0.0.1', () => {
|
||||
inspectorProxy.addWebSocketListener(httpServer);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {InspectorProxy, runInspectorProxy};
|
12
node_modules/metro-inspector-proxy/src/types.js
generated
vendored
Normal file
12
node_modules/metro-inspector-proxy/src/types.js
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
"use strict"; // Page information received from the device. New page is created for
|
||||
// each new instance of VM and can appear when user reloads React Native
|
||||
// application.
|
93
node_modules/metro-inspector-proxy/src/types.js.flow
generated
vendored
Normal file
93
node_modules/metro-inspector-proxy/src/types.js.flow
generated
vendored
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// Page information received from the device. New page is created for
|
||||
// each new instance of VM and can appear when user reloads React Native
|
||||
// application.
|
||||
export type Page = {
|
||||
id: string,
|
||||
title: string,
|
||||
vm: string,
|
||||
app: string,
|
||||
...
|
||||
};
|
||||
|
||||
// Chrome Debugger Protocol message/event passed between device and debugger.
|
||||
export type WrappedEvent = {
|
||||
event: 'wrappedEvent',
|
||||
payload: {
|
||||
pageId: string,
|
||||
wrappedEvent: string,
|
||||
...
|
||||
},
|
||||
...
|
||||
};
|
||||
|
||||
// Request sent from Inspector Proxy to Device when new debugger is connected
|
||||
// to particular page.
|
||||
export type ConnectRequest = {
|
||||
event: 'connect',
|
||||
payload: {pageId: string, ...},
|
||||
...
|
||||
};
|
||||
|
||||
// Request sent from Inspector Proxy to Device to notify that debugger is
|
||||
// disconnected.
|
||||
export type DisconnectRequest = {
|
||||
event: 'disconnect',
|
||||
payload: {pageId: string, ...},
|
||||
...
|
||||
};
|
||||
|
||||
// Request sent from Inspector Proxy to Device to get a list of pages.
|
||||
export type GetPagesRequest = {event: 'getPages', ...};
|
||||
|
||||
// Response to GetPagesRequest containing a list of page infos.
|
||||
export type GetPagesResponse = {
|
||||
event: 'getPages',
|
||||
payload: Array<Page>,
|
||||
...
|
||||
};
|
||||
|
||||
// Union type for all possible messages sent from device to Inspector Proxy.
|
||||
export type MessageFromDevice =
|
||||
| GetPagesResponse
|
||||
| WrappedEvent
|
||||
| DisconnectRequest;
|
||||
|
||||
// Union type for all possible messages sent from Inspector Proxy to device.
|
||||
export type MessageToDevice =
|
||||
| GetPagesRequest
|
||||
| WrappedEvent
|
||||
| ConnectRequest
|
||||
| DisconnectRequest;
|
||||
|
||||
// Page description object that is sent in response to /json HTTP request from debugger.
|
||||
export type PageDescription = {
|
||||
id: string,
|
||||
description: string,
|
||||
title: string,
|
||||
faviconUrl: string,
|
||||
devtoolsFrontendUrl: string,
|
||||
type: string,
|
||||
webSocketDebuggerUrl: string,
|
||||
...
|
||||
};
|
||||
export type JsonPagesListResponse = Array<PageDescription>;
|
||||
|
||||
// Response to /json/version HTTP request from the debugger specifying browser type and
|
||||
// Chrome protocol version.
|
||||
export type JsonVersionResponse = {
|
||||
Browser: string,
|
||||
'Protocol-Version': string,
|
||||
...
|
||||
};
|
Reference in New Issue
Block a user