diff --git a/packages/react-native/ReactCommon/jsinspector-modern/CMakeLists.txt b/packages/react-native/ReactCommon/jsinspector-modern/CMakeLists.txt index 85d154cdc19..b59f732e1bd 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/CMakeLists.txt +++ b/packages/react-native/ReactCommon/jsinspector-modern/CMakeLists.txt @@ -16,5 +16,6 @@ add_library(jsinspector STATIC ${jsinspector_SRC}) target_include_directories(jsinspector PUBLIC ${REACT_COMMON_DIR}) target_link_libraries(jsinspector + folly_runtime glog ) diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorPackagerConnection.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InspectorPackagerConnection.cpp new file mode 100644 index 00000000000..536df0910f7 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorPackagerConnection.cpp @@ -0,0 +1,331 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "InspectorPackagerConnection.h" +#include "InspectorInterfaces.h" +#include "InspectorPackagerConnectionImpl.h" + +#include +#include +#include +#include +#include + +namespace facebook::react::jsinspector_modern { + +static constexpr const std::chrono::duration RECONNECT_DELAY = + std::chrono::milliseconds{2000}; +static constexpr const char* INVALID = ""; + +static folly::dynamic makePageIdPayload(std::string_view pageId) { + return folly::dynamic::object("id", pageId); +} + +/* +TODO(moti): Remove this comment, which is only here to help the diff tool +understand the relationship between this file and +RCTInspectorPackagerConnection.m. + +@implementation RCTInspectorPackagerConnection + +*/ + +// InspectorPackagerConnection::Impl method definitions + +std::shared_ptr +InspectorPackagerConnection::Impl::create( + std::string url, + std::string app, + std::unique_ptr delegate) { + // No make_shared because the constructor is private + return std::shared_ptr( + new InspectorPackagerConnection::Impl(url, app, std::move(delegate))); +} + +InspectorPackagerConnection::Impl::Impl( + std::string url, + std::string app, + std::unique_ptr delegate) + : url_(std::move(url)), + app_(std::move(app)), + delegate_(std::move(delegate)) {} + +void InspectorPackagerConnection::Impl::handleProxyMessage( + folly::const_dynamic_view message) { + std::string event = message.descend("event").string_or(INVALID); + if (event == "getPages") { + sendEvent("getPages", pages()); + } else if (event == "wrappedEvent") { + handleWrappedEvent(message.descend("payload")); + } else if (event == "connect") { + handleConnect(message.descend("payload")); + } else if (event == "disconnect") { + handleDisconnect(message.descend("payload")); + } else { + LOG(ERROR) << "Unknown event: " << event; + } +} + +void InspectorPackagerConnection::Impl::sendEventToAllConnections( + std::string event) { + for (auto& connection : inspectorConnections_) { + connection.second->sendMessage(event); + } +} + +void InspectorPackagerConnection::Impl::closeAllConnections() { + for (auto& connection : inspectorConnections_) { + connection.second->disconnect(); + } + inspectorConnections_.clear(); +} + +void InspectorPackagerConnection::Impl::handleConnect( + folly::const_dynamic_view payload) { + std::string pageId = payload.descend("pageId").string_or(INVALID); + auto existingConnectionIt = inspectorConnections_.find(pageId); + if (existingConnectionIt != inspectorConnections_.end()) { + auto existingConnection = std::move(existingConnectionIt->second); + inspectorConnections_.erase(existingConnectionIt); + existingConnection->disconnect(); + LOG(WARNING) << "Already connected: " << pageId; + return; + } + int pageIdInt; + try { + pageIdInt = std::stoi(pageId); + } catch (...) { + LOG(ERROR) << "Invalid page id: " << pageId; + return; + } + auto remoteConnection = + std::make_unique( + weak_from_this(), pageId); + auto& inspector = getInspectorInstance(); + auto inspectorConnection = + inspector.connect(pageIdInt, std::move(remoteConnection)); + inspectorConnections_.emplace(pageId, std::move(inspectorConnection)); +} + +void InspectorPackagerConnection::Impl::handleDisconnect( + folly::const_dynamic_view payload) { + std::string pageId = payload.descend("pageId").string_or(INVALID); + auto inspectorConnection = removeConnectionForPage(pageId); + if (inspectorConnection) { + inspectorConnection->disconnect(); + } +} + +std::unique_ptr +InspectorPackagerConnection::Impl::removeConnectionForPage(std::string pageId) { + auto it = inspectorConnections_.find(pageId); + if (it != inspectorConnections_.end()) { + auto connection = std::move(it->second); + inspectorConnections_.erase(it); + return connection; + } + return nullptr; +} + +void InspectorPackagerConnection::Impl::handleWrappedEvent( + folly::const_dynamic_view payload) { + std::string pageId = payload.descend("pageId").string_or(INVALID); + std::string wrappedEvent = payload.descend("wrappedEvent").string_or(INVALID); + auto connectionIt = inspectorConnections_.find(pageId); + if (connectionIt == inspectorConnections_.end()) { + LOG(WARNING) << "Not connected to page: " << pageId + << " , failed trying to handle event: " << wrappedEvent; + return; + } + connectionIt->second->sendMessage(wrappedEvent); +} + +folly::dynamic InspectorPackagerConnection::Impl::pages() { + auto& inspector = getInspectorInstance(); + auto pages = inspector.getPages(); + folly::dynamic array = folly::dynamic::array(); + + for (const auto& page : pages) { + array.push_back(folly::dynamic::object("id", std::to_string(page.id))( + "title", page.title)("app", app_)("vm", page.vm)); + } + return array; +} + +void InspectorPackagerConnection::Impl::sendWrappedEvent( + std::string pageId, + std::string message) { + sendEvent( + "wrappedEvent", + folly::dynamic::object("pageId", pageId)("wrappedEvent", message)); +} + +void InspectorPackagerConnection::Impl::sendEvent( + std::string event, + folly::dynamic payload) { + folly::dynamic message = + folly::dynamic::object("event", event)("payload", payload); + sendToPackager(message); +} + +void InspectorPackagerConnection::Impl::didFailWithError( + std::optional posixCode, + std::string error) { + if (webSocket_) { + abort(posixCode, "WebSocket exception", error); + } +} + +void InspectorPackagerConnection::Impl::didReceiveMessage( + std::string_view message) { + folly::dynamic parsedJSON; + try { + parsedJSON = folly::parseJson(message); + } catch (const folly::json::parse_error& e) { + LOG(ERROR) << "Unrecognized inspector message, string was not valid JSON: " + << e.what(); + return; + } + handleProxyMessage(std::move(parsedJSON)); +} + +void InspectorPackagerConnection::Impl::didClose() { + webSocket_.reset(); + closeAllConnections(); + if (!closed_) { + reconnect(); + } +} + +bool InspectorPackagerConnection::Impl::isConnected() const { + return webSocket_ != nullptr; +} + +void InspectorPackagerConnection::Impl::connect() { + if (closed_) { + LOG(ERROR) + << "Illegal state: Can't connect after having previously been closed."; + return; + } + webSocket_ = delegate_->connectWebSocket(url_, weak_from_this()); +} + +void InspectorPackagerConnection::Impl::reconnect() { + if (closed_) { + LOG(ERROR) + << "Illegal state: Can't reconnect after having previously been closed."; + return; + } + + if (!suppressConnectionErrors_) { + LOG(WARNING) << "Couldn't connect to packager, will silently retry"; + suppressConnectionErrors_ = true; + } + + delegate_->scheduleCallback( + [weakSelf = weak_from_this()] { + auto strongSelf = weakSelf.lock(); + if (strongSelf && !strongSelf->closed_) { + strongSelf->connect(); + } + }, + RECONNECT_DELAY); +} + +void InspectorPackagerConnection::Impl::closeQuietly() { + closed_ = true; + disposeWebSocket(); +} + +void InspectorPackagerConnection::Impl::sendToPackager(folly::dynamic message) { + if (!webSocket_) { + return; + } + + webSocket_->send(folly::toJson(message)); +} + +void InspectorPackagerConnection::Impl::abort( + std::optional posixCode, + const std::string& message, + const std::string& cause) { + // Don't log ECONNREFUSED at all; it's expected in cases where the server + // isn't listening. + if (posixCode != ECONNREFUSED) { + LOG(INFO) << "Error occurred, shutting down websocket connection: " + << message << " " << cause; + } + closeAllConnections(); + disposeWebSocket(); +} + +void InspectorPackagerConnection::Impl::disposeWebSocket() { + webSocket_.reset(); +} + +/* + +@end + +TODO(moti): Remove this comment, which is only here to help the diff tool +understand the relationship between this file and +RCTInspectorPackagerConnection.m. + +@implementation RCTInspectorRemoteConnection + +*/ + +// InspectorPackagerConnection::RemoteConnectionImpl method definitions + +InspectorPackagerConnection::RemoteConnectionImpl::RemoteConnectionImpl( + std::weak_ptr owningPackagerConnection, + std::string pageId) + : owningPackagerConnection_(owningPackagerConnection), + pageId_(std::move(pageId)) {} + +void InspectorPackagerConnection::RemoteConnectionImpl::onMessage( + std::string message) { + auto owningPackagerConnectionStrong = owningPackagerConnection_.lock(); + if (!owningPackagerConnectionStrong) { + return; + } + owningPackagerConnectionStrong->sendWrappedEvent(pageId_, message); +} + +void InspectorPackagerConnection::RemoteConnectionImpl::onDisconnect() { + auto owningPackagerConnectionStrong = owningPackagerConnection_.lock(); + if (owningPackagerConnectionStrong) { + owningPackagerConnectionStrong->sendEvent( + "disconnect", makePageIdPayload(pageId_)); + } +} + +// InspectorPackagerConnection method definitions + +InspectorPackagerConnection::InspectorPackagerConnection( + std::string url, + std::string app, + std::unique_ptr delegate) + : impl_(Impl::create(url, app, std::move(delegate))) {} + +bool InspectorPackagerConnection::isConnected() const { + return impl_->isConnected(); +} + +void InspectorPackagerConnection::connect() { + impl_->connect(); +} + +void InspectorPackagerConnection::closeQuietly() { + impl_->closeQuietly(); +} + +void InspectorPackagerConnection::sendEventToAllConnections(std::string event) { + impl_->sendEventToAllConnections(event); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorPackagerConnection.h b/packages/react-native/ReactCommon/jsinspector-modern/InspectorPackagerConnection.h new file mode 100644 index 00000000000..bfb080c3659 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorPackagerConnection.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "WebSocketInterfaces.h" + +#include +#include +#include +#include +#include +#include + +namespace facebook::react::jsinspector_modern { + +class InspectorPackagerConnectionDelegate; + +/** + * A platform-agnostic implementation of the "device" side of the React Native + * inspector-proxy protocol. The protocol multiplexes one or more debugger + * connections over a single socket. + * InspectorPackagerConnection will automatically attempt to reconnect after a + * delay if the connection fails or is lost. + */ +class InspectorPackagerConnection { + public: + /** + * Creates a new connection instance. Connections start in the disconnected + * state; connect() should be called to establish a connection. + * \param url The WebSocket URL where the inspector-proxy server is listening. + * \param app The name of the application being debugged. + * \param delegate An interface to platform-specific methods for creating a + * WebSocket, scheduling async work, etc. + */ + InspectorPackagerConnection( + std::string url, + std::string app, + std::unique_ptr delegate); + bool isConnected() const; + void connect(); + void closeQuietly(); + void sendEventToAllConnections(std::string event); + + private: + class Impl; + class RemoteConnectionImpl; + + std::shared_ptr impl_; +}; + +/** + * An interface implemented by each supported platform to provide + * platform-specific functionality required by InspectorPackagerConnection. + */ +class InspectorPackagerConnectionDelegate { + public: + virtual ~InspectorPackagerConnectionDelegate() = default; + + /** + * Creates a new WebSocket connection. The WebSocket must be in a connected + * state when created, and automatically disconnect when destroyed. + */ + virtual std::unique_ptr connectWebSocket( + const std::string& url, + std::weak_ptr delegate) = 0; + + /** + * Schedules a function to run after a delay. If the function is called + * asynchronously, the implementer of InspectorPackagerConnectionDelegate + * is responsible for thread safety (e.g. scheduling the callback on the same + * thread that called scheduleCallback, or otherwise ensuring + * synchronization). The callback MAY be dropped and never called, e.g. if the + * application is terminating. + */ + virtual void scheduleCallback( + std::function callback, + std::chrono::milliseconds delayMs) = 0; +}; + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorPackagerConnectionImpl.h b/packages/react-native/ReactCommon/jsinspector-modern/InspectorPackagerConnectionImpl.h new file mode 100644 index 00000000000..3a2363908a7 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorPackagerConnectionImpl.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "InspectorInterfaces.h" +#include "InspectorPackagerConnection.h" + +#include +#include + +namespace facebook::react::jsinspector_modern { + +/** + * Internals of InspectorPackagerConnection. + */ +class InspectorPackagerConnection::Impl + : public IWebSocketDelegate, + // Used to generate `weak_ptr`s we can pass around. + public std::enable_shared_from_this { + public: + /** + * Implements InspectorPackagerConnection's constructor. + */ + static std::shared_ptr create( + std::string url, + std::string app, + std::unique_ptr delegate); + + // InspectorPackagerConnection's public API + bool isConnected() const; + void connect(); + void closeQuietly(); + void sendEventToAllConnections(std::string event); + std::unique_ptr removeConnectionForPage(std::string pageId); + + // Exposed for RemoteConnectionImpl's use + void sendEvent(std::string event, folly::dynamic payload); + // Exposed for RemoteConnectionImpl's use + void sendWrappedEvent(std::string pageId, std::string message); + + private: + Impl( + std::string url, + std::string app, + std::unique_ptr delegate); + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + + void handleDisconnect(folly::const_dynamic_view payload); + void handleConnect(folly::const_dynamic_view payload); + void handleWrappedEvent(folly::const_dynamic_view wrappedEvent); + void handleProxyMessage(folly::const_dynamic_view message); + folly::dynamic pages(); + void reconnect(); + void closeAllConnections(); + void disposeWebSocket(); + void sendToPackager(folly::dynamic message); + + void abort( + std::optional posixCode, + const std::string& message, + const std::string& cause); + + // IWebSocketDelegate methods + virtual void didFailWithError(std::optional posixCode, std::string error) + override; + virtual void didReceiveMessage(std::string_view message) override; + virtual void didClose() override; + + std::string url_; + std::string app_; + std::unique_ptr delegate_; + + std::unordered_map> + inspectorConnections_; + std::unique_ptr webSocket_; + bool closed_{false}; + bool suppressConnectionErrors_{false}; +}; + +class InspectorPackagerConnection::RemoteConnectionImpl + : public IRemoteConnection { + public: + RemoteConnectionImpl( + std::weak_ptr owningPackagerConnection, + std::string pageId); + + // IRemoteConnection methods + void onMessage(std::string message) override; + void onDisconnect() override; + + private: + std::weak_ptr owningPackagerConnection_; + std::string pageId_; +}; + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec b/packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec index a2299673d1e..dc128078ade 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec +++ b/packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec @@ -16,6 +16,9 @@ else source[:tag] = "v#{version}" end +folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_CFG_NO_COROUTINES=1 -DFOLLY_HAVE_CLOCK_GETTIME=1 -DFOLLY_HAVE_CLOCK_GETTIME=1 -Wno-comma -Wno-shorten-64-to-32' +folly_version = '2023.08.07.00' + Pod::Spec.new do |s| s.name = "React-jsinspector" s.version = version @@ -27,9 +30,12 @@ Pod::Spec.new do |s| s.source = source s.source_files = "*.{cpp,h}" s.header_dir = 'jsinspector' - s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => "c++20" } - + s.compiler_flags = folly_compiler_flags + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/..\" \"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/RCT-Folly\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/fmt/include\"", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20" + } s.dependency "glog" - - add_dependency(s, "React-nativeconfig") + s.dependency "RCT-Folly", folly_version + s.dependency "React-nativeconfig" end diff --git a/packages/react-native/ReactCommon/jsinspector-modern/WebSocketInterfaces.h b/packages/react-native/ReactCommon/jsinspector-modern/WebSocketInterfaces.h new file mode 100644 index 00000000000..06cabca5a01 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/WebSocketInterfaces.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react::jsinspector_modern { + +/** + * Simplified interface to a WebSocket connection. + * The socket MUST be initially open when constructed. + */ +class IWebSocket { + public: + /** + * If still connected when destroyed, the socket MUST automatically send an + * "end of session" message and disconnect. + */ + virtual ~IWebSocket() = default; + + /** + * Sends a message over the socket. This function may be called on any thread + * without synchronization. + * \param message Message to send, in UTF-8 encoding. + */ + virtual void send(std::string_view message) = 0; +}; + +class IWebSocketDelegate { + public: + virtual ~IWebSocketDelegate() = default; + + /** + * Called when the socket has encountered an error. + * \param posixCode POSIX errno value if available, otherwise nullopt. + * \param error Error description. + */ + virtual void didFailWithError( + std::optional posixCode, + std::string error) = 0; + + /** + * Called when a message has been received from the socket. + * \param message Message received, in UTF-8 encoding. + */ + virtual void didReceiveMessage(std::string_view message) = 0; + + /** + * Called when the socket has been closed. + */ + virtual void didClose() = 0; +}; + +} // namespace facebook::react::jsinspector_modern