C++ InspectorPackagerConnection (#41977)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/41977

Changelog: [Internal]

Adds a new C++ implementation of `InspectorPackagerConnection`, intended to eventually replace `RCTInspectorPackagerConnection` on iOS and `InspectorPackagerConnection.java` on Android.

The main *new* abstraction in the C++ version is the `InspectorPackagerConnectionDelegate` interface, which will allow each platform to plug in its own scheduler and WebSocket implementation

This is almost entirely a direct translation of the Objective-C implementation to C++, so I've modelled it as a file copy in source control for ease of review. We may iterate further on the API at a later date, especially once the old implementations are gone.

Reviewed By: huntie

Differential Revision: D52134592

fbshipit-source-id: b4778b9c4fd424c4fa8d23bb9171629874e50e73
This commit is contained in:
Moti Zilberman
2023-12-20 13:53:09 -08:00
committed by Facebook GitHub Bot
parent cfa02eec50
commit db0d178b8f
6 changed files with 590 additions and 4 deletions
@@ -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
)
@@ -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 <folly/dynamic.h>
#include <folly/json.h>
#include <glog/logging.h>
#include <cerrno>
#include <chrono>
namespace facebook::react::jsinspector_modern {
static constexpr const std::chrono::duration RECONNECT_DELAY =
std::chrono::milliseconds{2000};
static constexpr const char* INVALID = "<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>
InspectorPackagerConnection::Impl::create(
std::string url,
std::string app,
std::unique_ptr<InspectorPackagerConnectionDelegate> delegate) {
// No make_shared because the constructor is private
return std::shared_ptr<InspectorPackagerConnection::Impl>(
new InspectorPackagerConnection::Impl(url, app, std::move(delegate)));
}
InspectorPackagerConnection::Impl::Impl(
std::string url,
std::string app,
std::unique_ptr<InspectorPackagerConnectionDelegate> 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<InspectorPackagerConnection::RemoteConnectionImpl>(
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<ILocalConnection>
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<int> 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<int> 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<InspectorPackagerConnection::Impl> 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<InspectorPackagerConnectionDelegate> 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
@@ -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 <chrono>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
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<InspectorPackagerConnectionDelegate> delegate);
bool isConnected() const;
void connect();
void closeQuietly();
void sendEventToAllConnections(std::string event);
private:
class Impl;
class RemoteConnectionImpl;
std::shared_ptr<Impl> 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<IWebSocket> connectWebSocket(
const std::string& url,
std::weak_ptr<IWebSocketDelegate> 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<void(void)> callback,
std::chrono::milliseconds delayMs) = 0;
};
} // namespace facebook::react::jsinspector_modern
@@ -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 <folly/dynamic.h>
#include <unordered_map>
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<InspectorPackagerConnection::Impl> {
public:
/**
* Implements InspectorPackagerConnection's constructor.
*/
static std::shared_ptr<Impl> create(
std::string url,
std::string app,
std::unique_ptr<InspectorPackagerConnectionDelegate> delegate);
// InspectorPackagerConnection's public API
bool isConnected() const;
void connect();
void closeQuietly();
void sendEventToAllConnections(std::string event);
std::unique_ptr<ILocalConnection> 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<InspectorPackagerConnectionDelegate> 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<int> posixCode,
const std::string& message,
const std::string& cause);
// IWebSocketDelegate methods
virtual void didFailWithError(std::optional<int> 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<InspectorPackagerConnectionDelegate> delegate_;
std::unordered_map<std::string, std::unique_ptr<ILocalConnection>>
inspectorConnections_;
std::unique_ptr<IWebSocket> webSocket_;
bool closed_{false};
bool suppressConnectionErrors_{false};
};
class InspectorPackagerConnection::RemoteConnectionImpl
: public IRemoteConnection {
public:
RemoteConnectionImpl(
std::weak_ptr<InspectorPackagerConnection::Impl> owningPackagerConnection,
std::string pageId);
// IRemoteConnection methods
void onMessage(std::string message) override;
void onDisconnect() override;
private:
std::weak_ptr<InspectorPackagerConnection::Impl> owningPackagerConnection_;
std::string pageId_;
};
} // namespace facebook::react::jsinspector_modern
@@ -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
@@ -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 <optional>
#include <string>
#include <string_view>
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<int> 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