feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,6 @@
---
Checks: '>
clang-diagnostic-*,
'
InheritParentConfig: true
...

View File

@@ -0,0 +1,26 @@
# 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.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
add_compile_options(
-fexceptions
-std=c++20)
file(GLOB jsinspector_SRC CONFIGURE_DEPENDS *.cpp)
# jsinspector contains singletons that hold app-global state (InspectorFlags, InspectorImpl).
# Placing it in a shared library makes the singletons safe to use from arbitrary shared libraries
# (even ones that don't depend on one another).
add_library(jsinspector SHARED ${jsinspector_SRC})
target_include_directories(jsinspector PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(jsinspector
folly_runtime
glog
react_featureflags
runtimeexecutor
)

View File

@@ -0,0 +1,27 @@
# jsinspector-modern concepts
## CDP object model
### Target
A debuggable entity that a debugger frontend can connect to.
### Target Delegate
An interface between a Target class and the underlying debuggable entity. For example, PageTargetDelegate is used by PageTarget to send page-related events to the native platform implementation.
### Target Controller
A private interface exposed by a Target class to its Sessions/Agents. For example, PageTargetController is used by PageAgent to safely access the page's PageTargetDelegate, without exposing PageTarget's other private state.
### Session
A single connection between a debugger frontend and a target. There can be multiple active sessions connected to the same target.
### Agent
A handler for a subset of CDP messages for a specific target as part of a specific session.
### Agent Delegate
An interface between an Agent class and some integration-specific, per-session logic/state it relies on (that does not fit in a Target Delegate). For example, a RuntimeAgentDelegate is used by RuntimeAgent to host Hermes's native CDP handler and delegate messages to it. The interface may look exactly like an Agent (purely CDP messages in/out) or there may be a more involved API to expose state/functionality needed by the Agent.

View File

@@ -0,0 +1,66 @@
/*
* 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 "ExecutionContext.h"
namespace facebook::react::jsinspector_modern {
namespace {
template <class>
inline constexpr bool always_false_v = false;
} // namespace
bool ExecutionContextSelector::matches(
const ExecutionContextDescription& context) const noexcept {
// Exhaustiveness checking based on the example in
// https://en.cppreference.com/w/cpp/utility/variant/visit.
return std::visit(
[&context](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, AllContexts>) {
return true;
} else if constexpr (std::is_same_v<T, ContextId>) {
return context.id == arg;
} else if constexpr (std::is_same_v<T, ContextName>) {
return context.name == arg;
} else {
static_assert(always_false_v<T>, "non-exhaustive visitor");
}
},
value_);
// Prevent the compiler from thinking always_false_v is unused when the
// visitor is (correctly) exhaustive.
(void)always_false_v<void>;
}
ExecutionContextSelector ExecutionContextSelector::byId(int32_t id) {
return ExecutionContextSelector{id};
}
ExecutionContextSelector ExecutionContextSelector::byName(std::string name) {
return ExecutionContextSelector{std::move(name)};
}
ExecutionContextSelector ExecutionContextSelector::all() {
return ExecutionContextSelector{AllContexts{}};
}
bool matchesAny(
const ExecutionContextDescription& context,
const ExecutionContextSelectorSet& selectors) {
for (const auto& selector : selectors) {
if (selector.matches(context)) {
return true;
}
}
return false;
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,109 @@
/*
* 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 "UniqueMonostate.h"
#include <cinttypes>
#include <optional>
#include <string>
#include <unordered_set>
#include <variant>
namespace facebook::react::jsinspector_modern {
struct ExecutionContextDescription {
int32_t id{};
std::string origin{""};
std::string name{"<anonymous>"};
std::optional<std::string> uniqueId;
};
/**
* A type-safe selector for execution contexts.
*/
class ExecutionContextSelector {
public:
/**
* Returns true iff this selector matches \c context.
*/
bool matches(const ExecutionContextDescription& context) const noexcept;
/**
* Returns a new selector that matches only the given execution context ID.
*/
static ExecutionContextSelector byId(int32_t id);
/**
* Returns a new selector that matches only the given execution context name.
*/
static ExecutionContextSelector byName(std::string name);
/**
* Returns a new selector that matches any execution context.
*/
static ExecutionContextSelector all();
ExecutionContextSelector() = delete;
ExecutionContextSelector(const ExecutionContextSelector& other) = default;
ExecutionContextSelector(ExecutionContextSelector&& other) noexcept = default;
ExecutionContextSelector& operator=(const ExecutionContextSelector& other) =
default;
ExecutionContextSelector& operator=(
ExecutionContextSelector&& other) noexcept = default;
~ExecutionContextSelector() = default;
inline bool operator==(const ExecutionContextSelector& other) const noexcept {
return value_ == other.value_;
}
private:
/**
* Marker type used to represent "all execution contexts".
*
* Q: What is a UniqueMonostate?
* A: std::monostate, but it's distinct from other UniqueMonostate<...>s, so
* you can use multiple of them in the same variant without ambiguity.
*/
using AllContexts = UniqueMonostate<0>;
using ContextId = int32_t;
using ContextName = std::string;
using Representation = std::variant<AllContexts, ContextId, ContextName>;
explicit inline ExecutionContextSelector(Representation&& r) : value_(r) {}
Representation value_;
friend struct std::hash<
facebook::react::jsinspector_modern::ExecutionContextSelector>;
};
using ExecutionContextSelectorSet =
std::unordered_set<ExecutionContextSelector>;
bool matchesAny(
const ExecutionContextDescription& context,
const ExecutionContextSelectorSet& selectors);
} // namespace facebook::react::jsinspector_modern
namespace std {
template <>
struct hash<::facebook::react::jsinspector_modern::ExecutionContextSelector> {
size_t operator()(
const ::facebook::react::jsinspector_modern::ExecutionContextSelector&
selector) const {
return hash<::facebook::react::jsinspector_modern::
ExecutionContextSelector::Representation>{}(
selector.value_);
}
};
} // namespace std

View File

@@ -0,0 +1,19 @@
/*
* 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 "ExecutionContextManager.h"
#include <cassert>
namespace facebook::react::jsinspector_modern {
int32_t ExecutionContextManager::allocateExecutionContextId() {
assert(nextExecutionContextId_ != INT32_MAX);
return nextExecutionContextId_++;
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,25 @@
/*
* 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 <cinttypes>
namespace facebook::react::jsinspector_modern {
/**
* Generates unique execution context IDs.
*/
class ExecutionContextManager {
public:
int32_t allocateExecutionContextId();
private:
int32_t nextExecutionContextId_{1};
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,70 @@
/*
* 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 "FallbackRuntimeAgentDelegate.h"
#include <chrono>
#include <string>
using namespace std::chrono;
using namespace std::literals::string_view_literals;
using namespace std::literals::string_literals;
namespace facebook::react::jsinspector_modern {
#define ANSI_WEIGHT_BOLD "\x1B[1m"
#define ANSI_WEIGHT_RESET "\x1B[22m"
#define ANSI_STYLE_ITALIC "\x1B[3m"
#define ANSI_STYLE_RESET "\x1B[23m"
#define ANSI_COLOR_BG_YELLOW "\x1B[48;2;253;247;231m"
FallbackRuntimeAgentDelegate::FallbackRuntimeAgentDelegate(
FrontendChannel frontendChannel,
const SessionState& sessionState,
std::string engineDescription)
: frontendChannel_(frontendChannel), engineDescription_(engineDescription) {
if (sessionState.isLogDomainEnabled) {
sendFallbackRuntimeWarning();
}
}
bool FallbackRuntimeAgentDelegate::handleRequest(
const cdp::PreparsedRequest& req) {
if (req.method == "Log.enable") {
sendFallbackRuntimeWarning();
// The parent Agent should send a response.
return false;
}
// The parent Agent should send a response or report an error.
return false;
}
void FallbackRuntimeAgentDelegate::sendFallbackRuntimeWarning() {
sendWarningLogEntry(
"The current JavaScript engine, " ANSI_STYLE_ITALIC + engineDescription_ +
ANSI_STYLE_RESET
", does not support debugging over the Chrome DevTools Protocol. "
"See https://reactnative.dev/docs/debugging for more information.");
}
void FallbackRuntimeAgentDelegate::sendWarningLogEntry(std::string_view text) {
frontendChannel_(
folly::toJson(folly::dynamic::object("method", "Log.entryAdded")(
"params",
folly::dynamic::object(
"entry",
folly::dynamic::object(
"timestamp",
duration_cast<milliseconds>(
system_clock::now().time_since_epoch())
.count())("source", "other")(
"level", "warning")("text", text)))));
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,66 @@
/*
* 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 "SessionState.h"
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/RuntimeAgent.h>
namespace facebook::react::jsinspector_modern {
/**
* A RuntimeAgentDelegate that handles requests from the Chrome DevTools
* Protocol for a JavaScript runtime that does not support debugging.
*/
class FallbackRuntimeAgentDelegate : public RuntimeAgentDelegate {
public:
/**
* \param frontendChannel A channel used to send responses and events to the
* frontend.
* \param sessionState The state of the current debugger session.
* \param engineDescription A description of the JavaScript engine being
* debugged. This string will be used in messages sent to the frontend.
*/
FallbackRuntimeAgentDelegate(
FrontendChannel frontendChannel,
const SessionState& sessionState,
std::string engineDescription);
/**
* Handle a CDP request. The response will be sent over the provided
* \c FrontendChannel synchronously or asynchronously.
* \param req The parsed request.
* \returns true if this agent has responded, or will respond asynchronously,
* to the request (with either a success or error message). False if the
* agent expects another agent to respond to the request instead.
*/
bool handleRequest(const cdp::PreparsedRequest& req) override;
private:
/**
* Send a user-facing message explaining that this is not a debuggable
* runtime. You must ensure that the frontend has enabled Log notifications
* (using Log.enable) prior to calling this function.
*/
void sendFallbackRuntimeWarning();
/**
* Send a simple Log.entryAdded notification with the given
* \param text. You must ensure that the frontend has enabled Log
* notifications (using Log.enable) prior to calling this function. In Chrome
* DevTools, the message will appear in the Console tab along with regular
* console messages.
*/
void sendWarningLogEntry(std::string_view text);
FrontendChannel frontendChannel_;
std::string engineDescription_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,58 @@
/*
* 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 "InspectorFlags.h"
#include <glog/logging.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
namespace facebook::react::jsinspector_modern {
InspectorFlags& InspectorFlags::getInstance() {
static InspectorFlags instance;
return instance;
}
InspectorFlags::InspectorFlags()
: enableModernCDPRegistry_(
ReactNativeFeatureFlags::inspectorEnableModernCDPRegistry()),
enableCxxInspectorPackagerConnection_(
ReactNativeFeatureFlags::
inspectorEnableCxxInspectorPackagerConnection()) {}
bool InspectorFlags::getEnableModernCDPRegistry() const {
assertFlagsMatchUpstream();
return enableModernCDPRegistry_;
}
bool InspectorFlags::getEnableCxxInspectorPackagerConnection() const {
assertFlagsMatchUpstream();
return enableCxxInspectorPackagerConnection_ ||
// If we are using the modern CDP registry, then we must also use the C++
// InspectorPackagerConnection implementation.
enableModernCDPRegistry_;
}
void InspectorFlags::assertFlagsMatchUpstream() const {
if (inconsistentFlagsStateLogged_) {
return;
}
if (enableModernCDPRegistry_ !=
ReactNativeFeatureFlags::inspectorEnableModernCDPRegistry() ||
enableCxxInspectorPackagerConnection_ !=
ReactNativeFeatureFlags::
inspectorEnableCxxInspectorPackagerConnection()) {
LOG(ERROR)
<< "[InspectorFlags] Error: One or more ReactNativeFeatureFlags values "
<< "have changed during the global app lifetime. This may lead to "
<< "inconsistent inspector behaviour. Please quit and restart the app.";
inconsistentFlagsStateLogged_ = true;
}
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,46 @@
/*
* 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>
namespace facebook::react::jsinspector_modern {
/**
* A container for all inspector related feature flags (Meyers singleton
* pattern). Enforces that flag values are static for the lifetime of the app.
*/
class InspectorFlags {
public:
static InspectorFlags& getInstance();
/**
* Flag determining if the modern CDP backend should be enabled.
*/
bool getEnableModernCDPRegistry() const;
/**
* Flag determining if the C++ implementation of InspectorPackagerConnection
* should be used instead of the per-platform one.
*/
bool getEnableCxxInspectorPackagerConnection() const;
private:
InspectorFlags();
InspectorFlags(const InspectorFlags&) = delete;
InspectorFlags& operator=(const InspectorFlags&) = delete;
~InspectorFlags() = default;
const bool enableModernCDPRegistry_;
const bool enableCxxInspectorPackagerConnection_;
mutable bool inconsistentFlagsStateLogged_;
void assertFlagsMatchUpstream() const;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,183 @@
/*
* 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 "InspectorInterfaces.h"
#include <cassert>
#include <list>
#include <mutex>
#include <tuple>
#include <unordered_map>
namespace facebook::react::jsinspector_modern {
// pure destructors in C++ are odd. You would think they don't want an
// implementation, but in fact the linker requires one. Define them to be
// empty so that people don't count on them for any particular behaviour.
IDestructible::~IDestructible() {}
ILocalConnection::~ILocalConnection() {}
IRemoteConnection::~IRemoteConnection() {}
IInspector::~IInspector() {}
IPageStatusListener::~IPageStatusListener() {}
const folly::dynamic targetCapabilitiesToDynamic(
const InspectorTargetCapabilities& capabilities) {
return folly::dynamic::object(
"nativePageReloads", capabilities.nativePageReloads)(
"nativeSourceCodeFetching", capabilities.nativeSourceCodeFetching);
}
namespace {
class InspectorImpl : public IInspector {
public:
int addPage(
const std::string& title,
const std::string& vm,
ConnectFunc connectFunc,
InspectorTargetCapabilities capabilities) override;
void removePage(int pageId) override;
std::vector<InspectorPageDescription> getPages() const override;
std::unique_ptr<ILocalConnection> connect(
int pageId,
std::unique_ptr<IRemoteConnection> remote) override;
void registerPageStatusListener(
std::weak_ptr<IPageStatusListener> listener) override;
private:
class Page {
public:
Page(
int id,
const std::string& title,
const std::string& vm,
ConnectFunc connectFunc,
InspectorTargetCapabilities capabilities);
operator InspectorPageDescription() const;
ConnectFunc getConnectFunc() const;
private:
int id_;
std::string title_;
std::string vm_;
ConnectFunc connectFunc_;
InspectorTargetCapabilities capabilities_;
};
mutable std::mutex mutex_;
int nextPageId_{1};
std::unordered_map<int, Page> pages_;
std::list<std::weak_ptr<IPageStatusListener>> listeners_;
};
InspectorImpl::Page::Page(
int id,
const std::string& title,
const std::string& vm,
ConnectFunc connectFunc,
InspectorTargetCapabilities capabilities)
: id_(id),
title_(title),
vm_(vm),
connectFunc_(std::move(connectFunc)),
capabilities_(std::move(capabilities)) {}
InspectorImpl::Page::operator InspectorPageDescription() const {
return InspectorPageDescription{
.id = id_,
.title = title_,
.vm = vm_,
.capabilities = capabilities_,
};
}
InspectorImpl::ConnectFunc InspectorImpl::Page::getConnectFunc() const {
return connectFunc_;
}
int InspectorImpl::addPage(
const std::string& title,
const std::string& vm,
ConnectFunc connectFunc,
InspectorTargetCapabilities capabilities) {
std::scoped_lock lock(mutex_);
int pageId = nextPageId_++;
assert(pages_.count(pageId) == 0 && "Unexpected duplicate page ID");
pages_.emplace(
pageId, Page{pageId, title, vm, std::move(connectFunc), capabilities});
return pageId;
}
void InspectorImpl::removePage(int pageId) {
std::scoped_lock lock(mutex_);
if (pages_.erase(pageId) != 0) {
for (auto listenerWeak : listeners_) {
if (auto listener = listenerWeak.lock()) {
listener->onPageRemoved(pageId);
}
}
}
}
std::vector<InspectorPageDescription> InspectorImpl::getPages() const {
std::scoped_lock lock(mutex_);
std::vector<InspectorPageDescription> inspectorPages;
for (auto& it : pages_) {
inspectorPages.push_back(InspectorPageDescription(it.second));
}
return inspectorPages;
}
std::unique_ptr<ILocalConnection> InspectorImpl::connect(
int pageId,
std::unique_ptr<IRemoteConnection> remote) {
IInspector::ConnectFunc connectFunc;
{
std::scoped_lock lock(mutex_);
auto it = pages_.find(pageId);
if (it != pages_.end()) {
connectFunc = it->second.getConnectFunc();
}
}
return connectFunc ? connectFunc(std::move(remote)) : nullptr;
}
void InspectorImpl::registerPageStatusListener(
std::weak_ptr<IPageStatusListener> listener) {
std::scoped_lock lock(mutex_);
// Remove expired listeners
for (auto it = listeners_.begin(); it != listeners_.end();) {
if (it->expired()) {
it = listeners_.erase(it);
} else {
++it;
}
}
listeners_.push_back(listener);
}
} // namespace
IInspector& getInspectorInstance() {
static InspectorImpl instance;
return instance;
}
std::unique_ptr<IInspector> makeTestInspectorInstance() {
return std::make_unique<InspectorImpl>();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,147 @@
/*
* 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 <folly/dynamic.h>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#ifndef JSINSPECTOR_EXPORT
#ifdef _MSC_VER
#ifdef CREATE_SHARED_LIBRARY
#define JSINSPECTOR_EXPORT __declspec(dllexport)
#else
#define JSINSPECTOR_EXPORT
#endif // CREATE_SHARED_LIBRARY
#else // _MSC_VER
#define JSINSPECTOR_EXPORT __attribute__((visibility("default")))
#endif // _MSC_VER
#endif // !defined(JSINSPECTOR_EXPORT)
namespace facebook::react::jsinspector_modern {
class IDestructible {
public:
virtual ~IDestructible() = 0;
};
struct InspectorTargetCapabilities {
const bool nativePageReloads = false;
const bool nativeSourceCodeFetching = false;
};
const folly::dynamic targetCapabilitiesToDynamic(
const InspectorTargetCapabilities& capabilities);
struct InspectorPageDescription {
const int id;
const std::string title;
const std::string vm;
const InspectorTargetCapabilities capabilities;
};
// Alias for backwards compatibility.
using InspectorPage = InspectorPageDescription;
/// IRemoteConnection allows the VM to send debugger messages to the client.
/// IRemoteConnection's methods are safe to call from any thread *if*
/// InspectorPackagerConnection.cpp is in use.
class JSINSPECTOR_EXPORT IRemoteConnection : public IDestructible {
public:
virtual ~IRemoteConnection() = 0;
virtual void onMessage(std::string message) = 0;
virtual void onDisconnect() = 0;
};
/// ILocalConnection allows the client to send debugger messages to the VM.
class JSINSPECTOR_EXPORT ILocalConnection : public IDestructible {
public:
virtual ~ILocalConnection() = 0;
virtual void sendMessage(std::string message) = 0;
/**
* Called by the inspector singleton to notify that the connection has been
* closed, either by the remote party or because the local page/VM is no
* longer registered with the inspector.
*/
virtual void disconnect() = 0;
};
class JSINSPECTOR_EXPORT IPageStatusListener : public IDestructible {
public:
virtual ~IPageStatusListener() = 0;
virtual void onPageRemoved(int pageId) = 0;
};
/// IInspector tracks debuggable JavaScript targets (pages).
class JSINSPECTOR_EXPORT IInspector : public IDestructible {
public:
using ConnectFunc = std::function<std::unique_ptr<ILocalConnection>(
std::unique_ptr<IRemoteConnection>)>;
virtual ~IInspector() = 0;
/**
* Add a page to the list of inspectable pages.
* Callers are responsible for calling removePage when the page is no longer
* expecting connections.
* \param connectFunc a function that will be called to establish a
* connection. \c connectFunc may return nullptr to reject the connection
* (e.g. if the page is in the process of shutting down).
* \returns the ID assigned to the new page.
*/
virtual int addPage(
const std::string& title,
const std::string& vm,
ConnectFunc connectFunc,
InspectorTargetCapabilities capabilities = {}) = 0;
/// removePage is called by the VM to remove a page from the list of
/// debuggable pages.
virtual void removePage(int pageId) = 0;
/// getPages is called by the client to list all debuggable pages.
virtual std::vector<InspectorPageDescription> getPages() const = 0;
/**
* Called by InspectorPackagerConnection to initiate a debugging session with
* the given page.
* \returns an ILocalConnection that can be used to send messages to the
* page, or nullptr if the connection has been rejected.
*/
virtual std::unique_ptr<ILocalConnection> connect(
int pageId,
std::unique_ptr<IRemoteConnection> remote) = 0;
/**
* registerPageStatusListener registers a listener that will receive events
* when pages are removed.
*/
virtual void registerPageStatusListener(
std::weak_ptr<IPageStatusListener> listener) = 0;
};
/// getInspectorInstance retrieves the singleton inspector that tracks all
/// debuggable pages in this process.
extern IInspector& getInspectorInstance();
/// makeTestInspectorInstance creates an independent inspector instance that
/// should only be used in tests.
extern std::unique_ptr<IInspector> makeTestInspectorInstance();
/**
* A callback that can be used to send debugger messages (method responses and
* events) to the frontend. The message must be a JSON-encoded string.
* The callback may be called from any thread.
*/
using FrontendChannel = std::function<void(std::string_view messageJson)>;
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,365 @@
/*
* 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>
using namespace std::literals;
namespace facebook::react::jsinspector_modern {
static constexpr const std::chrono::duration RECONNECT_DELAY =
std::chrono::milliseconds{2000};
static constexpr const char* INVALID = "<invalid>";
// 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
std::shared_ptr<InspectorPackagerConnection::Impl> impl(
new InspectorPackagerConnection::Impl(url, app, std::move(delegate)));
getInspectorInstance().registerPageStatusListener(impl);
return impl;
}
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") {
sendToPackager(
folly::dynamic::object("event", "getPages")("payload", 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 : inspectorSessions_) {
connection.second.localConnection->sendMessage(event);
}
}
void InspectorPackagerConnection::Impl::closeAllConnections() {
for (auto& connection : inspectorSessions_) {
connection.second.localConnection->disconnect();
}
inspectorSessions_.clear();
}
void InspectorPackagerConnection::Impl::handleConnect(
folly::const_dynamic_view payload) {
std::string pageId = payload.descend("pageId").string_or(INVALID);
auto existingConnectionIt = inspectorSessions_.find(pageId);
if (existingConnectionIt != inspectorSessions_.end()) {
auto existingConnection = std::move(existingConnectionIt->second);
inspectorSessions_.erase(existingConnectionIt);
existingConnection.localConnection->disconnect();
LOG(WARNING) << "Already connected: " << pageId;
return;
}
int pageIdInt;
try {
pageIdInt = std::stoi(pageId);
} catch (...) {
LOG(ERROR) << "Invalid page id: " << pageId;
return;
}
auto sessionId = nextSessionId_++;
auto remoteConnection =
std::make_unique<InspectorPackagerConnection::Impl::RemoteConnection>(
weak_from_this(), pageId, sessionId);
auto& inspector = getInspectorInstance();
auto inspectorConnection =
inspector.connect(pageIdInt, std::move(remoteConnection));
if (!inspectorConnection) {
LOG(INFO) << "Connection to page " << pageId << " rejected";
// RemoteConnection::onDisconnect(), if the connection even calls it, will
// be a no op (because the session is not added to `inspectorSessions_`), so
// let's always notify the remote client of the disconnection ourselves.
sendToPackager(folly::dynamic::object("event", "disconnect")(
"payload", folly::dynamic::object("pageId", pageId)));
return;
}
inspectorSessions_.emplace(
pageId,
Session{
.localConnection = std::move(inspectorConnection),
.sessionId = sessionId});
}
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 = inspectorSessions_.find(pageId);
if (it != inspectorSessions_.end()) {
auto connection = std::move(it->second);
inspectorSessions_.erase(it);
return std::move(connection.localConnection);
}
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 = inspectorSessions_.find(pageId);
if (connectionIt == inspectorSessions_.end()) {
LOG(WARNING) << "Not connected to page: " << pageId
<< " , failed trying to handle event: " << wrappedEvent;
return;
}
connectionIt->second.localConnection->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) {
folly::dynamic pageDescription = folly::dynamic::object;
pageDescription["id"] = std::to_string(page.id);
pageDescription["title"] = page.title + " [C++ connection]";
pageDescription["app"] = app_;
pageDescription["vm"] = page.vm;
pageDescription["capabilities"] =
targetCapabilitiesToDynamic(page.capabilities);
array.push_back(pageDescription);
}
return array;
}
void InspectorPackagerConnection::Impl::didFailWithError(
std::optional<int> posixCode,
std::string error) {
if (webSocket_) {
abort(posixCode, "WebSocket exception", error);
}
if (!closed_ && posixCode != ECONNREFUSED) {
reconnect();
}
}
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();
}
}
void InspectorPackagerConnection::Impl::onPageRemoved(int pageId) {
auto connection = removeConnectionForPage(std::to_string(pageId));
if (connection) {
connection->disconnect();
}
}
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 (reconnectPending_) {
return;
}
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;
}
reconnectPending_ = true;
delegate_->scheduleCallback(
[weakSelf = weak_from_this()] {
auto strongSelf = weakSelf.lock();
if (strongSelf && !strongSelf->closed_) {
strongSelf->reconnectPending_ = false;
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::scheduleSendToPackager(
folly::dynamic message,
SessionId sourceSessionId,
std::string sourcePageId) {
delegate_->scheduleCallback(
[weakSelf = weak_from_this(),
message = std::move(message),
sourceSessionId,
sourcePageId]() mutable {
auto strongSelf = weakSelf.lock();
if (!strongSelf) {
return;
}
auto sessionIt = strongSelf->inspectorSessions_.find(sourcePageId);
if (sessionIt != strongSelf->inspectorSessions_.end() &&
sessionIt->second.sessionId == sourceSessionId) {
strongSelf->sendToPackager(std::move(message));
}
},
0ms);
}
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();
}
// InspectorPackagerConnection::Impl::RemoteConnection method definitions
InspectorPackagerConnection::Impl::RemoteConnection::RemoteConnection(
std::weak_ptr<InspectorPackagerConnection::Impl> owningPackagerConnection,
std::string pageId,
SessionId sessionId)
: owningPackagerConnection_(owningPackagerConnection),
pageId_(std::move(pageId)),
sessionId_(sessionId) {}
void InspectorPackagerConnection::Impl::RemoteConnection::onMessage(
std::string message) {
auto owningPackagerConnectionStrong = owningPackagerConnection_.lock();
if (!owningPackagerConnectionStrong) {
return;
}
owningPackagerConnectionStrong->scheduleSendToPackager(
folly::dynamic::object("event", "wrappedEvent")(
"payload",
folly::dynamic::object("pageId", pageId_)("wrappedEvent", message)),
sessionId_,
pageId_);
}
void InspectorPackagerConnection::Impl::RemoteConnection::onDisconnect() {
auto owningPackagerConnectionStrong = owningPackagerConnection_.lock();
if (owningPackagerConnectionStrong) {
owningPackagerConnectionStrong->scheduleSendToPackager(
folly::dynamic::object("event", "disconnect")(
"payload", folly::dynamic::object("pageId", pageId_)),
sessionId_,
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

View File

@@ -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;
const 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 a thread
* that has unique access to the InspectorPackagerConnection instance, or
* otherwise ensuring synchronization). The callback MAY be dropped and never
* called if no further callbacks are being accepted, 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

View File

@@ -0,0 +1,127 @@
/*
* 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,
public IPageStatusListener,
// Used to generate `weak_ptr`s we can pass around.
public std::enable_shared_from_this<InspectorPackagerConnection::Impl> {
public:
using SessionId = uint32_t;
/**
* 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);
/**
* Send a message to the packager as soon as possible. This method is safe
* to call from any thread. The connection may be closed before the message
* is sent, in which case the message will be dropped. The message is also
* dropped if the session is no longer valid.
*/
void scheduleSendToPackager(
folly::dynamic message,
SessionId sourceSessionId,
std::string sourcePageId);
private:
struct Session {
std::unique_ptr<ILocalConnection> localConnection;
SessionId sessionId;
};
class RemoteConnection;
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;
// IPageStatusListener methods
virtual void onPageRemoved(int pageId) override;
const std::string url_;
const std::string app_;
const std::unique_ptr<InspectorPackagerConnectionDelegate> delegate_;
std::unordered_map<std::string, Session> inspectorSessions_;
std::unique_ptr<IWebSocket> webSocket_;
bool closed_{false};
bool suppressConnectionErrors_{false};
// Whether a reconnection is currently pending.
bool reconnectPending_{false};
SessionId nextSessionId_{1};
};
class InspectorPackagerConnection::Impl::RemoteConnection
: public IRemoteConnection {
public:
RemoteConnection(
std::weak_ptr<InspectorPackagerConnection::Impl> owningPackagerConnection,
std::string pageId,
SessionId sessionId);
// IRemoteConnection methods
void onMessage(std::string message) override;
void onDisconnect() override;
private:
const std::weak_ptr<InspectorPackagerConnection::Impl>
owningPackagerConnection_;
const std::string pageId_;
const SessionId sessionId_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,39 @@
/*
* 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 "InspectorUtilities.h"
#include <cassert>
namespace facebook::react::jsinspector_modern {
CallbackLocalConnection::CallbackLocalConnection(
std::function<void(std::string)> handler)
: handler_(std::move(handler)) {}
void CallbackLocalConnection::sendMessage(std::string message) {
assert(handler_ && "Handler has been disconnected");
handler_(std::move(message));
}
void CallbackLocalConnection::disconnect() {
handler_ = nullptr;
}
RAIIRemoteConnection::RAIIRemoteConnection(
std::unique_ptr<IRemoteConnection> remote)
: remote_(std::move(remote)) {}
void RAIIRemoteConnection::onMessage(std::string message) {
remote_->onMessage(std::move(message));
}
RAIIRemoteConnection::~RAIIRemoteConnection() {
remote_->onDisconnect();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,52 @@
/*
* 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"
// Utilities that are useful when integrating with InspectorInterfaces.h but
// do not need to be exported.
namespace facebook::react::jsinspector_modern {
/**
* Wraps a callback function in ILocalConnection.
*/
class CallbackLocalConnection : public ILocalConnection {
public:
/**
* Creates a new Connection that uses the given callback to send messages to
* the backend.
*/
explicit CallbackLocalConnection(std::function<void(std::string)> handler);
void sendMessage(std::string message) override;
void disconnect() override;
private:
std::function<void(std::string)> handler_;
};
/**
* Wraps an IRemoteConnection in a simpler interface that calls `onDisconnect`
* implicitly upon destruction.
*/
class RAIIRemoteConnection {
public:
explicit RAIIRemoteConnection(std::unique_ptr<IRemoteConnection> remote);
void onMessage(std::string message);
~RAIIRemoteConnection();
private:
std::unique_ptr<IRemoteConnection> remote_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,75 @@
/*
* 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 <jsinspector-modern/InstanceAgent.h>
#include "RuntimeTarget.h"
namespace facebook::react::jsinspector_modern {
InstanceAgent::InstanceAgent(
FrontendChannel frontendChannel,
InstanceTarget& target,
SessionState& sessionState)
: frontendChannel_(frontendChannel),
target_(target),
sessionState_(sessionState) {
(void)target_;
}
bool InstanceAgent::handleRequest(const cdp::PreparsedRequest& req) {
if (req.method == "Runtime.enable") {
maybeSendExecutionContextCreatedNotification();
// Fall through
}
if (runtimeAgent_ && runtimeAgent_->handleRequest(req)) {
return true;
}
return false;
}
void InstanceAgent::setCurrentRuntime(RuntimeTarget* runtimeTarget) {
auto previousRuntimeAgent = std::move(runtimeAgent_);
if (runtimeTarget) {
runtimeAgent_ = runtimeTarget->createAgent(frontendChannel_, sessionState_);
} else {
runtimeAgent_.reset();
}
if (!sessionState_.isRuntimeDomainEnabled) {
return;
}
if (previousRuntimeAgent != nullptr) {
auto& previousContext =
previousRuntimeAgent->getExecutionContextDescription();
folly::dynamic params =
folly::dynamic::object("executionContextId", previousContext.id);
if (previousContext.uniqueId.has_value()) {
params["executionContextUniqueId"] = *previousContext.uniqueId;
}
folly::dynamic contextDestroyed = folly::dynamic::object(
"method", "Runtime.executionContextDestroyed")("params", params);
frontendChannel_(folly::toJson(contextDestroyed));
}
maybeSendExecutionContextCreatedNotification();
}
void InstanceAgent::maybeSendExecutionContextCreatedNotification() {
if (runtimeAgent_ != nullptr) {
auto& newContext = runtimeAgent_->getExecutionContextDescription();
folly::dynamic params = folly::dynamic::object(
"context",
folly::dynamic::object("id", newContext.id)(
"origin", newContext.origin)("name", newContext.name));
if (newContext.uniqueId.has_value()) {
params["uniqueId"] = *newContext.uniqueId;
}
folly::dynamic contextCreated = folly::dynamic::object(
"method", "Runtime.executionContextCreated")("params", params);
frontendChannel_(folly::toJson(contextCreated));
}
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,66 @@
/*
* 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 "RuntimeTarget.h"
#include "SessionState.h"
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/Parsing.h>
#include <jsinspector-modern/RuntimeAgent.h>
#include <functional>
namespace facebook::react::jsinspector_modern {
class InstanceTarget;
/**
* An Agent that handles requests from the Chrome DevTools Protocol for the
* given InstanceTarget.
*/
class InstanceAgent final {
public:
/**
* \param frontendChannel A channel used to send responses and events to the
* frontend.
* \param target The InstanceTarget that this agent is attached to. The
* caller is responsible for ensuring that the InstanceTarget outlives this
* object.
* \param sessionState The state of the session that created this agent.
*/
explicit InstanceAgent(
FrontendChannel frontendChannel,
InstanceTarget& target,
SessionState& sessionState);
/**
* Handle a CDP request. The response will be sent over the provided
* \c FrontendChannel synchronously or asynchronously.
* \param req The parsed request.
*/
bool handleRequest(const cdp::PreparsedRequest& req);
/**
* Replace the current RuntimeAgent pageAgent_ with a new one
* connected to the new RuntimeTarget.
* \param runtime The new runtime target. May be nullptr to indicate
* there's no current debuggable runtime.
*/
void setCurrentRuntime(RuntimeTarget* runtime);
private:
void maybeSendExecutionContextCreatedNotification();
FrontendChannel frontendChannel_;
InstanceTarget& target_;
std::shared_ptr<RuntimeAgent> runtimeAgent_;
SessionState& sessionState_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,82 @@
/*
* 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 "InstanceAgent.h"
#include "SessionState.h"
#include <jsinspector-modern/InstanceTarget.h>
namespace facebook::react::jsinspector_modern {
std::shared_ptr<InstanceTarget> InstanceTarget::create(
std::shared_ptr<ExecutionContextManager> executionContextManager,
InstanceTargetDelegate& delegate,
VoidExecutor executor) {
std::shared_ptr<InstanceTarget> instanceTarget{
new InstanceTarget(executionContextManager, delegate)};
instanceTarget->setExecutor(executor);
return instanceTarget;
}
InstanceTarget::InstanceTarget(
std::shared_ptr<ExecutionContextManager> executionContextManager,
InstanceTargetDelegate& delegate)
: delegate_(delegate),
executionContextManager_(std::move(executionContextManager)) {
(void)delegate_;
}
InstanceTargetDelegate::~InstanceTargetDelegate() {}
std::shared_ptr<InstanceAgent> InstanceTarget::createAgent(
FrontendChannel channel,
SessionState& sessionState) {
auto instanceAgent =
std::make_shared<InstanceAgent>(channel, *this, sessionState);
instanceAgent->setCurrentRuntime(currentRuntime_.get());
agents_.insert(instanceAgent);
return instanceAgent;
}
InstanceTarget::~InstanceTarget() {
// Agents are owned by the session, not by InstanceTarget, but
// they hold an InstanceTarget& that we must guarantee is valid.
assert(
agents_.empty() &&
"InstanceAgent objects must be destroyed before their InstanceTarget. Did you call PageTarget::unregisterInstance()?");
}
RuntimeTarget& InstanceTarget::registerRuntime(
RuntimeTargetDelegate& delegate,
RuntimeExecutor jsExecutor) {
assert(!currentRuntime_ && "Only one Runtime allowed");
currentRuntime_ = RuntimeTarget::create(
ExecutionContextDescription{
.id = executionContextManager_->allocateExecutionContextId(),
.origin = "",
.name = "main",
.uniqueId = std::nullopt},
delegate,
jsExecutor,
makeVoidExecutor(executorFromThis()));
agents_.forEach([currentRuntime = &*currentRuntime_](InstanceAgent& agent) {
agent.setCurrentRuntime(currentRuntime);
});
return *currentRuntime_;
}
void InstanceTarget::unregisterRuntime(RuntimeTarget& Runtime) {
assert(
currentRuntime_ && currentRuntime_.get() == &Runtime &&
"Invalid unregistration");
agents_.forEach(
[](InstanceAgent& agent) { agent.setCurrentRuntime(nullptr); });
currentRuntime_.reset();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,97 @@
/*
* 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 "ExecutionContextManager.h"
#include "RuntimeTarget.h"
#include "ScopedExecutor.h"
#include "SessionState.h"
#include "WeakList.h"
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/RuntimeAgent.h>
#include <list>
#include <memory>
#include <optional>
namespace facebook::react::jsinspector_modern {
class InstanceAgent;
/**
* Receives events from an InstanceTarget. This is a shared interface that
* each React Native platform needs to implement in order to integrate with
* the debugging stack.
*/
class InstanceTargetDelegate {
public:
InstanceTargetDelegate() = default;
InstanceTargetDelegate(const InstanceTargetDelegate&) = delete;
InstanceTargetDelegate(InstanceTargetDelegate&&) = default;
InstanceTargetDelegate& operator=(const InstanceTargetDelegate&) = delete;
InstanceTargetDelegate& operator=(InstanceTargetDelegate&&) = default;
virtual ~InstanceTargetDelegate();
};
/**
* A Target that represents a single instance of React Native.
*/
class InstanceTarget : public EnableExecutorFromThis<InstanceTarget> {
public:
/**
* Constructs a new InstanceTarget.
* \param executionContextManager Assigns unique execution context IDs.
* \param delegate The object that will receive events from this target.
* The caller is responsible for ensuring that the delegate outlives this
* object.
* \param executor An executor that may be used to call methods on this
* InstanceTarget while it exists. \c create additionally guarantees that the
* executor will not be called after the InstanceTarget is destroyed.
*/
static std::shared_ptr<InstanceTarget> create(
std::shared_ptr<ExecutionContextManager> executionContextManager,
InstanceTargetDelegate& delegate,
VoidExecutor executor);
InstanceTarget(const InstanceTarget&) = delete;
InstanceTarget(InstanceTarget&&) = delete;
InstanceTarget& operator=(const InstanceTarget&) = delete;
InstanceTarget& operator=(InstanceTarget&&) = delete;
~InstanceTarget();
std::shared_ptr<InstanceAgent> createAgent(
FrontendChannel channel,
SessionState& sessionState);
RuntimeTarget& registerRuntime(
RuntimeTargetDelegate& delegate,
RuntimeExecutor executor);
void unregisterRuntime(RuntimeTarget& runtime);
private:
/**
* Constructs a new InstanceTarget. The caller must call setExecutor
* immediately afterwards.
* \param executionContextManager Assigns unique execution context IDs.
* \param delegate The object that will receive events from this target.
* The caller is responsible for ensuring that the delegate outlives this
* object.
*/
InstanceTarget(
std::shared_ptr<ExecutionContextManager> executionContextManager,
InstanceTargetDelegate& delegate);
InstanceTargetDelegate& delegate_;
std::shared_ptr<RuntimeTarget> currentRuntime_{nullptr};
WeakList<InstanceAgent> agents_;
std::shared_ptr<ExecutionContextManager> executionContextManager_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,153 @@
/*
* 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 <folly/dynamic.h>
#include <folly/json.h>
#include <jsinspector-modern/InstanceAgent.h>
#include <jsinspector-modern/PageAgent.h>
#include <jsinspector-modern/PageTarget.h>
#include <chrono>
using namespace std::chrono;
using namespace std::literals::string_view_literals;
namespace facebook::react::jsinspector_modern {
#define ANSI_WEIGHT_BOLD "\x1B[1m"
#define ANSI_WEIGHT_RESET "\x1B[22m"
#define ANSI_STYLE_ITALIC "\x1B[3m"
#define ANSI_STYLE_RESET "\x1B[23m"
#define ANSI_COLOR_BG_YELLOW "\x1B[48;2;253;247;231m"
static constexpr auto kModernCDPBackendNotice =
ANSI_COLOR_BG_YELLOW ANSI_WEIGHT_BOLD
"NOTE:" ANSI_WEIGHT_RESET " You are using the " ANSI_STYLE_ITALIC
"modern" ANSI_STYLE_RESET " CDP backend for React Native (PageTarget)."sv;
PageAgent::PageAgent(
FrontendChannel frontendChannel,
PageTargetController& targetController,
PageTarget::SessionMetadata sessionMetadata,
SessionState& sessionState)
: frontendChannel_(frontendChannel),
targetController_(targetController),
sessionMetadata_(std::move(sessionMetadata)),
sessionState_(sessionState) {}
void PageAgent::handleRequest(const cdp::PreparsedRequest& req) {
bool shouldSendOKResponse = false;
bool isFinishedHandlingRequest = false;
// Domain enable/disable requests: write to state (because we're the top-level
// Agent in the Session), trigger any side effects, and decide whether we are
// finished handling the request (or need to delegate to the InstanceAgent).
if (req.method == "Log.enable") {
sessionState_.isLogDomainEnabled = true;
// Send a log entry identifying the modern CDP backend.
sendInfoLogEntry(kModernCDPBackendNotice);
// Send a log entry with the integration name.
if (sessionMetadata_.integrationName) {
sendInfoLogEntry("Integration: " + *sessionMetadata_.integrationName);
}
shouldSendOKResponse = true;
isFinishedHandlingRequest = false;
} else if (req.method == "Log.disable") {
sessionState_.isLogDomainEnabled = false;
shouldSendOKResponse = true;
isFinishedHandlingRequest = false;
} else if (req.method == "Runtime.enable") {
sessionState_.isRuntimeDomainEnabled = true;
shouldSendOKResponse = true;
isFinishedHandlingRequest = false;
} else if (req.method == "Runtime.disable") {
sessionState_.isRuntimeDomainEnabled = false;
shouldSendOKResponse = true;
isFinishedHandlingRequest = false;
}
// Methods other than domain enables/disables: handle anything we know how
// to handle, and delegate to the InstanceAgent otherwise.
else if (req.method == "Page.reload") {
targetController_.getDelegate().onReload({
.ignoreCache = req.params.isObject() && req.params.count("ignoreCache")
? std::optional(req.params.at("ignoreCache").asBool())
: std::nullopt,
.scriptToEvaluateOnLoad =
req.params.isObject() && req.params.count("scriptToEvaluateOnLoad")
? std::optional(req.params.at("scriptToEvaluateOnLoad").asString())
: std::nullopt,
});
shouldSendOKResponse = true;
isFinishedHandlingRequest = true;
}
if (!isFinishedHandlingRequest && instanceAgent_ &&
instanceAgent_->handleRequest(req)) {
return;
}
if (shouldSendOKResponse) {
folly::dynamic res = folly::dynamic::object("id", req.id)(
"result", folly::dynamic::object());
std::string json = folly::toJson(res);
frontendChannel_(json);
return;
}
folly::dynamic res = folly::dynamic::object("id", req.id)(
"error",
folly::dynamic::object("code", -32601)(
"message", req.method + " not implemented yet"));
std::string json = folly::toJson(res);
frontendChannel_(json);
}
void PageAgent::sendInfoLogEntry(std::string_view text) {
frontendChannel_(
folly::toJson(folly::dynamic::object("method", "Log.entryAdded")(
"params",
folly::dynamic::object(
"entry",
folly::dynamic::object(
"timestamp",
duration_cast<milliseconds>(
system_clock::now().time_since_epoch())
.count())("source", "other")(
"level", "info")("text", text)))));
}
void PageAgent::setCurrentInstanceAgent(
std::shared_ptr<InstanceAgent> instanceAgent) {
auto previousInstanceAgent = std::move(instanceAgent_);
instanceAgent_ = std::move(instanceAgent);
if (!sessionState_.isRuntimeDomainEnabled) {
return;
}
if (previousInstanceAgent != nullptr) {
// TODO: Send Runtime.executionContextDestroyed here - at the moment we
// expect the runtime to do it for us.
// Because we can only have a single instance, we can report all contexts
// as cleared.
folly::dynamic contextsCleared =
folly::dynamic::object("method", "Runtime.executionContextsCleared");
frontendChannel_(folly::toJson(contextsCleared));
}
if (instanceAgent_) {
// TODO: Send Runtime.executionContextCreated here - at the moment we expect
// the runtime to do it for us.
}
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,89 @@
/*
* 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 "PageTarget.h"
#include "SessionState.h"
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/InstanceAgent.h>
#include <jsinspector-modern/Parsing.h>
#include <functional>
#include <string_view>
namespace facebook::react::jsinspector_modern {
class PageTarget;
class InstanceAgent;
class InstanceTarget;
/**
* An Agent that handles requests from the Chrome DevTools Protocol for the
* given page.
* The constructor, destructor and all public methods must be called on the
* same thread, which is also the thread where the associated PageTarget is
* constructed and managed.
*/
class PageAgent final {
public:
/**
* \param frontendChannel A channel used to send responses and events to the
* frontend.
* \param targetController An interface to the PageTarget that this agent is
* attached to. The caller is responsible for ensuring that the
* PageTargetDelegate and underlying PageTarget both outlive the agent.
* \param sessionMetadata Metadata about the session that created this agent.
* \param sessionState The state of the session that created this agent.
*/
PageAgent(
FrontendChannel frontendChannel,
PageTargetController& targetController,
PageTarget::SessionMetadata sessionMetadata,
SessionState& sessionState);
/**
* Handle a CDP request. The response will be sent over the provided
* \c FrontendChannel synchronously or asynchronously.
* \param req The parsed request.
*/
void handleRequest(const cdp::PreparsedRequest& req);
/**
* Replace the current InstanceAgent with the given one and notify the
* frontend about the new instance.
* \param agent The new InstanceAgent. May be null to signify that there is
* currently no active instance.
*/
void setCurrentInstanceAgent(std::shared_ptr<InstanceAgent> agent);
private:
/**
* Send a simple Log.entryAdded notification with the given
* \param text. You must ensure that the frontend has enabled Log
* notifications (using Log.enable) prior to calling this function. In Chrome
* DevTools, the message will appear in the Console tab along with regular
* console messages. The difference between Log.entryAdded and
* Runtime.consoleAPICalled is that the latter requires an execution context
* ID, which does not exist at the Page level.
*/
void sendInfoLogEntry(std::string_view text);
FrontendChannel frontendChannel_;
PageTargetController& targetController_;
const PageTarget::SessionMetadata sessionMetadata_;
std::shared_ptr<InstanceAgent> instanceAgent_;
/**
* A shared reference to the session's state. This is only safe to access
* during handleRequest and other method calls on the same thread.
*/
SessionState& sessionState_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,166 @@
/*
* 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 "PageTarget.h"
#include "InspectorInterfaces.h"
#include "InspectorUtilities.h"
#include "InstanceTarget.h"
#include "PageAgent.h"
#include "Parsing.h"
#include "SessionState.h"
#include <folly/dynamic.h>
#include <folly/json.h>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* A Session connected to a PageTarget, passing CDP messages to and from a
* PageAgent which it owns.
*/
class PageTargetSession {
public:
explicit PageTargetSession(
std::unique_ptr<IRemoteConnection> remote,
PageTargetController& targetController,
PageTarget::SessionMetadata sessionMetadata)
: remote_(std::make_shared<RAIIRemoteConnection>(std::move(remote))),
frontendChannel_(
[remoteWeak = std::weak_ptr(remote_)](std::string_view message) {
if (auto remote = remoteWeak.lock()) {
remote->onMessage(std::string(message));
}
}),
pageAgent_(
frontendChannel_,
targetController,
std::move(sessionMetadata),
state_) {}
/**
* Called by CallbackLocalConnection to send a message to this Session's
* Agent.
*/
void operator()(std::string message) {
cdp::PreparsedRequest request;
// Messages may be invalid JSON, or have unexpected types.
try {
request = cdp::preparse(message);
} catch (const cdp::ParseError& e) {
frontendChannel_(folly::toJson(folly::dynamic::object("id", nullptr)(
"error",
folly::dynamic::object("code", -32700)("message", e.what()))));
return;
} catch (const cdp::TypeError& e) {
frontendChannel_(folly::toJson(folly::dynamic::object("id", nullptr)(
"error",
folly::dynamic::object("code", -32600)("message", e.what()))));
return;
}
// Catch exceptions that may arise from accessing dynamic params during
// request handling.
try {
pageAgent_.handleRequest(request);
} catch (const cdp::TypeError& e) {
frontendChannel_(folly::toJson(folly::dynamic::object("id", request.id)(
"error",
folly::dynamic::object("code", -32600)("message", e.what()))));
return;
}
}
/**
* Replace the current instance agent inside pageAgent_ with a new one
* connected to the new InstanceTarget.
* \param instance The new instance target. May be nullptr to indicate
* there's no current instance.
*/
void setCurrentInstance(InstanceTarget* instance) {
if (instance) {
pageAgent_.setCurrentInstanceAgent(
instance->createAgent(frontendChannel_, state_));
} else {
pageAgent_.setCurrentInstanceAgent(nullptr);
}
}
private:
// Owned by this instance, but shared (weakly) with the frontend channel
std::shared_ptr<RAIIRemoteConnection> remote_;
FrontendChannel frontendChannel_;
PageAgent pageAgent_;
SessionState state_;
};
std::shared_ptr<PageTarget> PageTarget::create(
PageTargetDelegate& delegate,
VoidExecutor executor) {
std::shared_ptr<PageTarget> pageTarget{new PageTarget(delegate)};
pageTarget->setExecutor(executor);
return pageTarget;
}
PageTarget::PageTarget(PageTargetDelegate& delegate)
: delegate_(delegate),
executionContextManager_{std::make_shared<ExecutionContextManager>()} {}
std::unique_ptr<ILocalConnection> PageTarget::connect(
std::unique_ptr<IRemoteConnection> connectionToFrontend,
SessionMetadata sessionMetadata) {
auto session = std::make_shared<PageTargetSession>(
std::move(connectionToFrontend), controller_, std::move(sessionMetadata));
session->setCurrentInstance(currentInstance_.get());
sessions_.insert(std::weak_ptr(session));
return std::make_unique<CallbackLocalConnection>(
[session](std::string message) { (*session)(message); });
}
PageTarget::~PageTarget() {
// Sessions are owned by InspectorPackagerConnection, not by PageTarget, but
// they hold a PageTarget& that we must guarantee is valid.
assert(
sessions_.empty() &&
"PageTargetSession objects must be destroyed before their PageTarget. Did you call getInspectorInstance().removePage()?");
}
PageTargetDelegate::~PageTargetDelegate() {}
InstanceTarget& PageTarget::registerInstance(InstanceTargetDelegate& delegate) {
assert(!currentInstance_ && "Only one instance allowed");
currentInstance_ = InstanceTarget::create(
executionContextManager_, delegate, makeVoidExecutor(executorFromThis()));
sessions_.forEach(
[currentInstance = &*currentInstance_](PageTargetSession& session) {
session.setCurrentInstance(currentInstance);
});
return *currentInstance_;
}
void PageTarget::unregisterInstance(InstanceTarget& instance) {
assert(
currentInstance_ && currentInstance_.get() == &instance &&
"Invalid unregistration");
sessions_.forEach(
[](PageTargetSession& session) { session.setCurrentInstance(nullptr); });
currentInstance_.reset();
}
PageTargetController::PageTargetController(PageTarget& target)
: target_(target) {}
PageTargetDelegate& PageTargetController::getDelegate() {
return target_.getDelegate();
}
bool PageTargetController::hasInstance() const {
return target_.hasInstance();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,190 @@
/*
* 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 "ExecutionContextManager.h"
#include "ScopedExecutor.h"
#include "WeakList.h"
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/InstanceTarget.h>
#include <optional>
#include <string>
#ifndef JSINSPECTOR_EXPORT
#ifdef _MSC_VER
#ifdef CREATE_SHARED_LIBRARY
#define JSINSPECTOR_EXPORT __declspec(dllexport)
#else
#define JSINSPECTOR_EXPORT
#endif // CREATE_SHARED_LIBRARY
#else // _MSC_VER
#define JSINSPECTOR_EXPORT __attribute__((visibility("default")))
#endif // _MSC_VER
#endif // !defined(JSINSPECTOR_EXPORT)
namespace facebook::react::jsinspector_modern {
class PageTargetSession;
class PageAgent;
class PageTarget;
/**
* Receives events from a PageTarget. This is a shared interface that each
* React Native platform needs to implement in order to integrate with the
* debugging stack.
*/
class PageTargetDelegate {
public:
PageTargetDelegate() = default;
PageTargetDelegate(const PageTargetDelegate&) = delete;
PageTargetDelegate(PageTargetDelegate&&) = default;
PageTargetDelegate& operator=(const PageTargetDelegate&) = delete;
PageTargetDelegate& operator=(PageTargetDelegate&&) = default;
// TODO(moti): This is 1:1 the shape of the corresponding CDP message -
// consider reusing typed/generated CDP interfaces when we have those.
struct PageReloadRequest {
// It isn't clear what the ignoreCache parameter of @cdp Page.reload should
// mean in React Native. We parse it, but don't do anything with it yet.
std::optional<bool> ignoreCache;
// TODO: Implement scriptToEvaluateOnLoad parameter of @cdp Page.reload.
std::optional<std::string> scriptToEvaluateOnLoad;
/**
* Equality operator, useful for unit tests
*/
inline bool operator==(const PageReloadRequest& rhs) const {
return ignoreCache == rhs.ignoreCache &&
scriptToEvaluateOnLoad == rhs.scriptToEvaluateOnLoad;
}
};
virtual ~PageTargetDelegate();
/**
* Called when the debugger requests a reload of the page. This is called on
* the thread on which messages are dispatched to the session (that is, where
* ILocalConnection::sendMessage was called).
*/
virtual void onReload(const PageReloadRequest& request) = 0;
};
/**
* The limited interface that PageTarget exposes to its associated
* sessions/agents.
*/
class PageTargetController final {
public:
explicit PageTargetController(PageTarget& target);
PageTargetDelegate& getDelegate();
bool hasInstance() const;
private:
PageTarget& target_;
};
/**
* The top-level Target in a React Native app. This is equivalent to the
* "Host" in React Native's architecture - the entity that manages the
* lifecycle of a React Instance.
*/
class JSINSPECTOR_EXPORT PageTarget
: public EnableExecutorFromThis<PageTarget> {
public:
struct SessionMetadata {
std::optional<std::string> integrationName;
};
/**
* Constructs a new PageTarget.
* \param delegate The PageTargetDelegate that will
* receive events from this PageTarget. The caller is responsible for ensuring
* that the PageTargetDelegate outlives this object.
* \param executor An executor that may be used to call methods on this
* PageTarget while it exists. \c create additionally guarantees that the
* executor will not be called after the PageTarget is destroyed.
*/
static std::shared_ptr<PageTarget> create(
PageTargetDelegate& delegate,
VoidExecutor executor);
PageTarget(const PageTarget&) = delete;
PageTarget(PageTarget&&) = delete;
PageTarget& operator=(const PageTarget&) = delete;
PageTarget& operator=(PageTarget&&) = delete;
~PageTarget();
/**
* Creates a new Session connected to this PageTarget, wrapped in an
* interface which is compatible with \c IInspector::addPage.
* The caller is responsible for destroying the connection before PageTarget
* is destroyed, on the same thread where PageTarget's constructor and
* destructor execute.
*/
std::unique_ptr<ILocalConnection> connect(
std::unique_ptr<IRemoteConnection> connectionToFrontend,
SessionMetadata sessionMetadata = {});
/**
* Registers an instance with this PageTarget.
* \param delegate The InstanceTargetDelegate that will receive events from
* this InstanceTarget. The caller is responsible for ensuring that the
* InstanceTargetDelegate outlives this object.
* \return An InstanceTarget reference representing the newly created
* instance. This reference is only valid until unregisterInstance is called
* (or the PageTarget is destroyed). \pre There isn't currently an instance
* registered with this PageTarget.
*/
InstanceTarget& registerInstance(InstanceTargetDelegate& delegate);
/**
* Unregisters an instance from this PageTarget.
* \param instance The InstanceTarget reference previously returned by
* registerInstance.
*/
void unregisterInstance(InstanceTarget& instance);
private:
/**
* Constructs a new PageTarget.
* The caller must call setExecutor immediately afterwards.
* \param delegate The PageTargetDelegate that will
* receive events from this PageTarget. The caller is responsible for ensuring
* that the PageTargetDelegate outlives this object.
*/
PageTarget(PageTargetDelegate& delegate);
PageTargetDelegate& delegate_;
WeakList<PageTargetSession> sessions_;
PageTargetController controller_{*this};
// executionContextManager_ is a shared_ptr to guarantee its validity while
// the InstanceTarget is alive (just in case the InstanceTarget ends up
// briefly outliving the PageTarget, which it generally shouldn't).
std::shared_ptr<ExecutionContextManager> executionContextManager_;
std::shared_ptr<InstanceTarget> currentInstance_{nullptr};
inline PageTargetDelegate& getDelegate() {
return delegate_;
}
inline bool hasInstance() const {
return currentInstance_ != nullptr;
}
// Necessary to allow PageAgent to access PageTarget's internals in a
// controlled way (i.e. only PageTargetController gets friend access, while
// PageAgent itself doesn't).
friend class PageTargetController;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,32 @@
/*
* 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 <folly/dynamic.h>
#include <folly/json.h>
#include <jsinspector-modern/Parsing.h>
namespace facebook::react::jsinspector_modern::cdp {
PreparsedRequest preparse(std::string_view message) {
folly::dynamic parsed = folly::parseJson(message);
return PreparsedRequest{
.id = parsed["id"].getInt(),
.method = parsed["method"].getString(),
.params = parsed.count("params") ? parsed["params"] : nullptr};
}
std::string PreparsedRequest::toJson() const {
folly::dynamic obj = folly::dynamic::object;
obj["id"] = id;
obj["method"] = method;
if (params != nullptr) {
obj["params"] = params;
}
return folly::toJson(obj);
}
} // namespace facebook::react::jsinspector_modern::cdp

View File

@@ -0,0 +1,69 @@
/*
* 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 <folly/dynamic.h>
#include <folly/json.h>
#include <string>
#include <string_view>
namespace facebook::react::jsinspector_modern {
namespace cdp {
using RequestId = long long;
/**
* An incoming CDP request that has been parsed into a more usable form.
*/
struct PreparsedRequest {
public:
/**
* The ID of the request.
*/
RequestId id;
/**
* The name of the method being invoked.
*/
std::string method;
/**
* The parameters passed to the method, if any.
*/
folly::dynamic params;
/**
* Equality operator, useful for unit tests
*/
inline bool operator==(const PreparsedRequest& rhs) const {
return id == rhs.id && method == rhs.method && params == rhs.params;
}
std::string toJson() const;
};
/**
* Parse a JSON-encoded CDP request into its constituent parts.
* \throws ParseError If the input cannot be parsed.
* \throws TypeError If the input does not conform to the expected format.
*/
PreparsedRequest preparse(std::string_view message);
/**
* A type error that may be thrown while preparsing a request, or while
* accessing dynamic params on a request.
*/
using TypeError = folly::TypeError;
/**
* A parse error that may be thrown while preparsing a request.
*/
using ParseError = folly::json::parse_error;
} // namespace cdp
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,60 @@
# 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.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
else
source[:tag] = "v#{version}"
end
folly_config = get_folly_config()
folly_compiler_flags = folly_config[:compiler_flags]
folly_version = folly_config[:version]
use_frameworks = ENV['USE_FRAMEWORKS'] != nil
header_dir = 'jsinspector-modern'
module_name = "jsinspector_modern"
Pod::Spec.new do |s|
s.name = "React-jsinspector"
s.version = version
s.summary = "-" # TODO
s.homepage = "https://reactnative.dev/"
s.license = package["license"]
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source
s.source_files = "*.{cpp,h}"
s.header_dir = 'jsinspector-modern'
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"
}.merge!(use_frameworks ? {
"PUBLIC_HEADERS_FOLDER_PATH" => "#{module_name}.framework/Headers/#{header_dir}"
} : {})
if use_frameworks
s.module_name = module_name
end
s.dependency "glog"
s.dependency "RCT-Folly", folly_version
s.dependency "React-featureflags"
s.dependency "DoubleConversion"
s.dependency "React-runtimeexecutor", version
s.dependency "React-jsi"
if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1"
s.dependency "hermes-engine"
end
end

View File

@@ -0,0 +1,16 @@
/*
* 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 <jsinspector-modern/ExecutionContext.h>
#include <jsinspector-modern/FallbackRuntimeAgentDelegate.h>
#include <jsinspector-modern/InstanceTarget.h>
#include <jsinspector-modern/PageTarget.h>
#include <jsinspector-modern/RuntimeTarget.h>
#include <jsinspector-modern/ScopedExecutor.h>
#include <jsinspector-modern/SessionState.h>

View File

@@ -0,0 +1,138 @@
/*
* 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 "RuntimeAgent.h"
#include "SessionState.h"
namespace facebook::react::jsinspector_modern {
RuntimeAgent::RuntimeAgent(
FrontendChannel frontendChannel,
RuntimeTargetController& targetController,
const ExecutionContextDescription& executionContextDescription,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate> delegate)
: frontendChannel_(std::move(frontendChannel)),
targetController_(targetController),
sessionState_(sessionState),
delegate_(std::move(delegate)),
executionContextDescription_(executionContextDescription) {
for (auto& [name, contextSelectors] : sessionState_.subscribedBindings) {
if (matchesAny(executionContextDescription_, contextSelectors)) {
targetController_.installBindingHandler(name);
}
}
}
bool RuntimeAgent::handleRequest(const cdp::PreparsedRequest& req) {
if (req.method == "Runtime.addBinding") {
std::string bindingName = req.params["name"].getString();
ExecutionContextSelector contextSelector = ExecutionContextSelector::all();
// TODO: Eventually, move execution context targeting out of RuntimeAgent.
// Right now, there's only ever one context (Runtime) in a Page, so we can
// handle it here for simplicity, and use session state to propagate
// bindings to the next RuntimeAgent.
if (req.params.count("executionContextId")) {
auto executionContextId = req.params["executionContextId"].getInt();
if (executionContextId < (int64_t)std::numeric_limits<int32_t>::min() ||
executionContextId > (int64_t)std::numeric_limits<int32_t>::max()) {
frontendChannel_(folly::toJson(folly::dynamic::object("id", req.id)(
"error",
folly::dynamic::object("code", -32602)(
"message", "Invalid execution context id"))));
return true;
}
contextSelector =
ExecutionContextSelector::byId((int32_t)executionContextId);
if (req.params.count("executionContextName")) {
frontendChannel_(folly::toJson(folly::dynamic::object("id", req.id)(
"error",
folly::dynamic::object("code", -32602)(
"message",
"executionContextName is mutually exclusive with executionContextId"))));
return true;
}
} else if (req.params.count("executionContextName")) {
contextSelector = ExecutionContextSelector::byName(
req.params["executionContextName"].getString());
}
if (contextSelector.matches(executionContextDescription_)) {
targetController_.installBindingHandler(bindingName);
}
sessionState_.subscribedBindings[bindingName].insert(contextSelector);
folly::dynamic res = folly::dynamic::object("id", req.id)(
"result", folly::dynamic::object());
std::string json = folly::toJson(res);
frontendChannel_(json);
return true;
}
if (req.method == "Runtime.removeBinding") {
// @cdp Runtime.removeBinding has no targeting by execution context. We
// interpret it to mean "unsubscribe, and stop installing the binding on
// all new contexts". This diverges slightly from V8, which continues
// to install the binding on new contexts after it's "removed", but *only*
// if the subscription is targeted by context name.
sessionState_.subscribedBindings.erase(req.params["name"].getString());
folly::dynamic res = folly::dynamic::object("id", req.id)(
"result", folly::dynamic::object());
std::string json = folly::toJson(res);
frontendChannel_(json);
return true;
}
if (delegate_) {
return delegate_->handleRequest(req);
}
return false;
}
void RuntimeAgent::notifyBindingCalled(
const std::string& bindingName,
const std::string& payload) {
// NOTE: When dispatching @cdp Runtime.bindingCalled notifications, we don't
// re-check whether the session is expecting notifications from the current
// context - only that it's subscribed to that binding name.
// Theoretically, this can result in over-sending notifications from contexts
// that the client no longer cares about, or never cared about to begin with
// (e.g. if the binding handler was installed by a previous session).
//
// React Native intentionally replicates this behavior for the sake of
// bug-for-bug compatibility with Chrome, but clients should probably not rely
// on it.
if (!sessionState_.subscribedBindings.count(bindingName)) {
return;
}
frontendChannel_(
folly::toJson(folly::dynamic::object("method", "Runtime.bindingCalled")(
"params",
folly::dynamic::object(
"executionContextId", executionContextDescription_.id)(
"name", bindingName)("payload", payload))));
}
RuntimeAgent::ExportedState RuntimeAgent::getExportedState() {
return {
.delegateState = delegate_ ? delegate_->getExportedState() : nullptr,
};
}
RuntimeAgent::~RuntimeAgent() {
// TODO: Eventually, there may be more than one Runtime per Page, and we'll
// need to store multiple agent states here accordingly. For now let's do
// the simple thing and assume (as we do elsewhere) that only one Runtime
// per Page can exist at a time.
sessionState_.lastRuntimeAgentExportedState = getExportedState();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,93 @@
/*
* 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 "RuntimeAgentDelegate.h"
#include "RuntimeTarget.h"
#include <jsinspector-modern/Parsing.h>
namespace facebook::react::jsinspector_modern {
class RuntimeTargetController;
struct SessionState;
/**
* An Agent that handles requests from the Chrome DevTools Protocol
* for a particular JS runtime instance. RuntimeAgent implements
* engine-agnostic functionality based on interfaces available in JSI /
* RuntimeTarget, and delegates engine-specific functionality to a
* RuntimeAgentDelegate.
*/
class RuntimeAgent final {
public:
/**
* \param frontendChannel A channel used to send responses and events to the
* frontend.
* \param targetController An interface to the RuntimeTarget that this agent
* is attached to. The caller is responsible for ensuring that the
* RuntimeTarget and controller outlive this object.
* \param executionContextDescription A description of the execution context
* represented by this runtime. This is used for disambiguating the
* source/destination of CDP messages when there are multiple runtimes
* (concurrently or over the life of a Page).
* \param sessionState The state of the session that created this agent.
* \param delegate The RuntimeAgentDelegate providing engine-specific
* CDP functionality.
*/
RuntimeAgent(
FrontendChannel frontendChannel,
RuntimeTargetController& targetController,
const ExecutionContextDescription& executionContextDescription,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate> delegate);
~RuntimeAgent();
/**
* Handle a CDP request. The response will be sent over the provided
* \c FrontendChannel synchronously or asynchronously. Performs any
* synchronization required between the thread on which this method is
* called and the thread where the JS runtime is executing.
* \param req The parsed request.
* \returns true if this agent has responded, or will respond asynchronously,
* to the request (with either a success or error message). False if the
* agent expects another agent to respond to the request instead.
*/
bool handleRequest(const cdp::PreparsedRequest& req);
inline const ExecutionContextDescription& getExecutionContextDescription()
const {
return executionContextDescription_;
}
void notifyBindingCalled(
const std::string& bindingName,
const std::string& payload);
struct ExportedState {
std::unique_ptr<RuntimeAgentDelegate::ExportedState> delegateState;
};
/**
* Export the RuntimeAgent's state, if available. This will be called
* shortly before the RuntimeAgent is destroyed to preserve state that may be
* needed when constructin a new RuntimeAgent.
*/
ExportedState getExportedState();
private:
FrontendChannel frontendChannel_;
RuntimeTargetController& targetController_;
SessionState& sessionState_;
const std::unique_ptr<RuntimeAgentDelegate> delegate_;
const ExecutionContextDescription executionContextDescription_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,51 @@
/*
* 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 <jsinspector-modern/Parsing.h>
namespace facebook::react::jsinspector_modern {
/**
* An Agent interface that handles requests from the Chrome DevTools Protocol
* for a particular JS runtime instance. The exact mechanism of sending
* responses/events to the frontend is left up to the implementation, but
* implementations SHOULD use FrontendChannel or a similar abstraction.
*/
class RuntimeAgentDelegate {
public:
class ExportedState {
public:
virtual ~ExportedState() = default;
};
virtual ~RuntimeAgentDelegate() = default;
/**
* Handle a CDP request. This implementation must perform any synchronization
* required between the thread on which this method is called and the thread
* where the JS runtime is executing.
* \returns true if this agent has responded, or will respond asynchronously,
* to the request (with either a success or error message). False if the
* agent expects another agent to respond to the request instead.
*/
virtual bool handleRequest(const cdp::PreparsedRequest& req) = 0;
/**
* Export RuntimeAgentDelegate-specific state that should persist across
* consecutive RuntimeTargets in this session.
* If the RuntimeTarget is destroyed and later logically replaced by a new
* one (e.g. as part of an Instance reload), the state returned here will be
* passed to \ref RuntimeTargetDelegate::createAgentDelegate.
*/
inline virtual std::unique_ptr<ExportedState> getExportedState() {
return std::make_unique<ExportedState>();
}
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,109 @@
/*
* 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 "SessionState.h"
#include <jsinspector-modern/RuntimeTarget.h>
using namespace facebook::jsi;
namespace facebook::react::jsinspector_modern {
std::shared_ptr<RuntimeTarget> RuntimeTarget::create(
const ExecutionContextDescription& executionContextDescription,
RuntimeTargetDelegate& delegate,
RuntimeExecutor jsExecutor,
VoidExecutor selfExecutor) {
std::shared_ptr<RuntimeTarget> runtimeTarget{
new RuntimeTarget(executionContextDescription, delegate, jsExecutor)};
runtimeTarget->setExecutor(selfExecutor);
return runtimeTarget;
}
RuntimeTarget::RuntimeTarget(
const ExecutionContextDescription& executionContextDescription,
RuntimeTargetDelegate& delegate,
RuntimeExecutor jsExecutor)
: executionContextDescription_(executionContextDescription),
delegate_(delegate),
jsExecutor_(jsExecutor) {}
std::shared_ptr<RuntimeAgent> RuntimeTarget::createAgent(
FrontendChannel channel,
SessionState& sessionState) {
auto runtimeAgentState =
std::move(sessionState.lastRuntimeAgentExportedState);
auto runtimeAgent = std::make_shared<RuntimeAgent>(
channel,
controller_,
executionContextDescription_,
sessionState,
delegate_.createAgentDelegate(
channel,
sessionState,
std::move(runtimeAgentState.delegateState),
executionContextDescription_));
agents_.insert(runtimeAgent);
return runtimeAgent;
}
RuntimeTarget::~RuntimeTarget() {
// Agents are owned by the session, not by RuntimeTarget, but
// they hold a RuntimeTarget& that we must guarantee is valid.
assert(
agents_.empty() &&
"RuntimeAgent objects must be destroyed before their RuntimeTarget. Did you call InstanceTarget::unregisterRuntime()?");
}
void RuntimeTarget::installBindingHandler(const std::string& bindingName) {
jsExecutor_([bindingName,
selfExecutor = executorFromThis()](jsi::Runtime& runtime) {
auto globalObj = runtime.global();
try {
auto bindingNamePropID = jsi::PropNameID::forUtf8(runtime, bindingName);
globalObj.setProperty(
runtime,
bindingNamePropID,
jsi::Function::createFromHostFunction(
runtime,
bindingNamePropID,
1,
[bindingName, selfExecutor](
jsi::Runtime& rt,
const jsi::Value&,
const jsi::Value* args,
size_t count) -> jsi::Value {
if (count != 1 || !args[0].isString()) {
throw jsi::JSError(
rt, "Invalid arguments: should be exactly one string.");
}
std::string payload = args[0].getString(rt).utf8(rt);
selfExecutor([bindingName, payload](auto& self) {
self.agents_.forEach([bindingName, payload](auto& agent) {
agent.notifyBindingCalled(bindingName, payload);
});
});
return jsi::Value::undefined();
}));
} catch (jsi::JSError&) {
// Per Chrome's implementation, @cdp Runtime.createBinding swallows
// JavaScript exceptions that occur while setting up the binding.
}
});
}
RuntimeTargetController::RuntimeTargetController(RuntimeTarget& target)
: target_(target) {}
void RuntimeTargetController::installBindingHandler(
const std::string& bindingName) {
target_.installBindingHandler(bindingName);
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,162 @@
/*
* 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 <ReactCommon/RuntimeExecutor.h>
#include "ExecutionContext.h"
#include "InspectorInterfaces.h"
#include "RuntimeAgent.h"
#include "ScopedExecutor.h"
#include "WeakList.h"
#include <memory>
#ifndef JSINSPECTOR_EXPORT
#ifdef _MSC_VER
#ifdef CREATE_SHARED_LIBRARY
#define JSINSPECTOR_EXPORT __declspec(dllexport)
#else
#define JSINSPECTOR_EXPORT
#endif // CREATE_SHARED_LIBRARY
#else // _MSC_VER
#define JSINSPECTOR_EXPORT __attribute__((visibility("default")))
#endif // _MSC_VER
#endif // !defined(JSINSPECTOR_EXPORT)
namespace facebook::react::jsinspector_modern {
class RuntimeAgent;
class RuntimeAgentDelegate;
class RuntimeTarget;
struct SessionState;
/**
* Receives events from a RuntimeTarget. This is a shared interface that
* each React Native platform needs to implement in order to integrate with
* the debugging stack.
*/
class RuntimeTargetDelegate {
public:
virtual ~RuntimeTargetDelegate() = default;
virtual std::unique_ptr<RuntimeAgentDelegate> createAgentDelegate(
FrontendChannel channel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription) = 0;
};
/**
* The limited interface that RuntimeTarget exposes to its connected agents.
*/
class RuntimeTargetController {
public:
explicit RuntimeTargetController(RuntimeTarget& target);
/**
* Adds a function with the given name on the runtime's global object, that
* when called will send a Runtime.bindingCalled event through all connected
* sessions that have registered to receive binding events for that name.
*/
void installBindingHandler(const std::string& bindingName);
private:
RuntimeTarget& target_;
};
/**
* A Target corresponding to a JavaScript runtime.
*/
class JSINSPECTOR_EXPORT RuntimeTarget
: public EnableExecutorFromThis<RuntimeTarget> {
public:
/**
* Constructs a new RuntimeTarget. The caller must call setExecutor
* immediately afterwards.
* \param executionContextDescription A description of the execution context
* represented by this runtime. This is used for disambiguating the
* source/destination of CDP messages when there are multiple runtimes
* (concurrently or over the life of a Page).
* \param delegate The object that will receive events from this target. The
* caller is responsible for
* ensuring that the delegate outlives this object.
* \param jsExecutor A RuntimeExecutor that can be used to schedule work on
* the JS runtime's thread. The executor's queue should be empty when
* RuntimeTarget is constructed (i.e. anything scheduled during the
* constructor should be executed before any user code is run).
* \param selfExecutor An executor that may be used to call methods on this
* RuntimeTarget while it exists. \c create additionally guarantees that the
* executor will not be called after the RuntimeTarget is destroyed.
*/
static std::shared_ptr<RuntimeTarget> create(
const ExecutionContextDescription& executionContextDescription,
RuntimeTargetDelegate& delegate,
RuntimeExecutor jsExecutor,
VoidExecutor selfExecutor);
RuntimeTarget(const RuntimeTarget&) = delete;
RuntimeTarget(RuntimeTarget&&) = delete;
RuntimeTarget& operator=(const RuntimeTarget&) = delete;
RuntimeTarget& operator=(RuntimeTarget&&) = delete;
~RuntimeTarget();
/**
* Create a new RuntimeAgent that can be used to debug the underlying JS VM.
* The agent will be destroyed when the session ends, the containing
* InstanceTarget is unregistered from its PageTarget, or the RuntimeAgent is
* unregistered from its InstanceTarget (whichever happens first).
* \param channel A thread-safe channel for sending CDP messages to the
* frontend.
* \returns The new agent, or nullptr if the runtime is not debuggable.
*/
std::shared_ptr<RuntimeAgent> createAgent(
FrontendChannel channel,
SessionState& sessionState);
private:
/**
* Constructs a new RuntimeTarget. The caller must call setExecutor
* immediately afterwards.
* \param executionContextDescription A description of the execution context
* represented by this runtime. This is used for disambiguating the
* source/destination of CDP messages when there are multiple runtimes
* (concurrently or over the life of a Page).
* \param delegate The object that will receive events from this target.
* The caller is responsible for ensuring that the delegate outlives this
* object.
* \param jsExecutor A RuntimeExecutor that can be used to schedule work on
* the JS runtime's thread. The executor's queue should be empty when
* RuntimeTarget is constructed (i.e. anything scheduled during the
* constructor should be executed before any user code is run).
*/
RuntimeTarget(
const ExecutionContextDescription& executionContextDescription,
RuntimeTargetDelegate& delegate,
RuntimeExecutor jsExecutor);
const ExecutionContextDescription executionContextDescription_;
RuntimeTargetDelegate& delegate_;
RuntimeExecutor jsExecutor_;
WeakList<RuntimeAgent> agents_;
RuntimeTargetController controller_{*this};
/**
* Adds a function with the given name on the runtime's global object, that
* when called will send a Runtime.bindingCalled event through all connected
* sessions that have registered to receive binding events for that name.
*/
void installBindingHandler(const std::string& bindingName);
// Necessary to allow RuntimeAgent to access RuntimeTarget's internals in a
// controlled way (i.e. only RuntimeTargetController gets friend access, while
// RuntimeAgent itself doesn't).
friend class RuntimeTargetController;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,97 @@
/*
* 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 <cassert>
#include <functional>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* Takes a function and calls it when it is safe to access the Self&
* parameter without locking. The function is not called if
* the underlying Self object is destroyed while the function is pending.
*/
template <typename Self>
using ScopedExecutor =
std::function<void(std::function<void(Self& self)>&& callback)>;
using VoidExecutor = std::function<void(std::function<void()>&& callback)>;
/**
* Creates a ScopedExecutor<Self> from a shared (weak) pointer to Self plus some
* base executor for a "parent" object of Self. The resulting executor will call
* the callback with a valid reference to Self iff Self is still alive.
*/
template <typename Self, typename Parent>
ScopedExecutor<Self> makeScopedExecutor(
std::shared_ptr<Self> self,
ScopedExecutor<Parent> executor) {
return makeScopedExecutor(self, makeVoidExecutor(executor));
}
/**
* Creates a ScopedExecutor<Self> from a shared (weak) pointer to Self plus some
* base executor. The resulting executor will call the callback with a valid
* reference to Self iff Self is still alive.
*/
template <typename Self>
ScopedExecutor<Self> makeScopedExecutor(
std::shared_ptr<Self> self,
VoidExecutor executor) {
return [self = std::weak_ptr(self), executor](auto&& callback) {
executor([self, callback = std::move(callback)]() {
auto lockedSelf = self.lock();
if (!lockedSelf) {
return;
}
callback(*lockedSelf);
});
};
}
/**
* Creates a VoidExecutor from a ScopedExecutor<Self> by ignoring the Self&
* parameter.
*/
template <typename Self>
VoidExecutor makeVoidExecutor(ScopedExecutor<Self> executor) {
return [executor](auto&& callback) {
executor([callback = std::move(callback)](Self&) { callback(); });
};
}
template <typename Self>
class EnableExecutorFromThis : public std::enable_shared_from_this<Self> {
public:
/**
* Returns an executor that can be used to safely invoke methods on Self.
* Must not be called during the constructor of Self.
*/
ScopedExecutor<Self> executorFromThis() {
assert(baseExecutor_);
return makeScopedExecutor(this->shared_from_this(), baseExecutor_);
}
template <typename Other>
void setExecutor(ScopedExecutor<Other> executor) {
setExecutor(makeVoidExecutor(executor));
}
void setExecutor(VoidExecutor executor) {
assert(executor);
assert(!baseExecutor_);
baseExecutor_ = std::move(executor);
}
private:
VoidExecutor baseExecutor_;
};
}; // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,51 @@
/*
* 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 "ExecutionContext.h"
#include "RuntimeAgent.h"
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
namespace facebook::react::jsinspector_modern {
struct SessionState {
public:
// TODO: Generalise this to arbitrary domains
bool isLogDomainEnabled{false};
bool isRuntimeDomainEnabled{false};
/**
* A map from binding names (registered during this session using @cdp
* Runtime.addBinding) to execution context selectors.
*
* Even though bindings get added to the global scope as
* functions that can outlive a session, they are treated as session state,
* matching Chrome's behaviour (a binding not added by the current session
* will not emit events on it).
*/
std::unordered_map<std::string, ExecutionContextSelectorSet>
subscribedBindings;
/**
* Stores the state object exported from the last main RuntimeAgent, if any,
* before it was destroyed.
*/
RuntimeAgent::ExportedState lastRuntimeAgentExportedState;
// Here, we will eventually allow RuntimeAgents to store their own arbitrary
// state (e.g. some sort of K/V storage of folly::dynamic?)
// TODO: Figure out a good model for restricting write access / preventing
// agents from unintentionally clobbering each other's state.
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,54 @@
/*
* 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 <functional>
namespace facebook::react::jsinspector_modern {
/**
* A template for easily creating empty types that are distinct from one
* another. Useful if you need to generate marker types for use in an
* std::variant, and a single std::monostate won't do.
*/
template <size_t key>
struct UniqueMonostate {
constexpr bool operator==(const UniqueMonostate<key>&) const noexcept {
return true;
}
constexpr bool operator!=(const UniqueMonostate<key>&) const noexcept {
return false;
}
constexpr bool operator<(const UniqueMonostate<key>&) const noexcept {
return false;
}
constexpr bool operator>(const UniqueMonostate<key>&) const noexcept {
return false;
}
constexpr bool operator<=(const UniqueMonostate<key>&) const noexcept {
return true;
}
constexpr bool operator>=(const UniqueMonostate<key>&) const noexcept {
return true;
}
};
} // namespace facebook::react::jsinspector_modern
namespace std {
template <size_t key>
struct hash<::facebook::react::jsinspector_modern::UniqueMonostate<key>> {
size_t operator()(
const ::facebook::react::jsinspector_modern::UniqueMonostate<key>&)
const noexcept {
return key;
}
};
} // namespace std

View File

@@ -0,0 +1,79 @@
/*
* 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 <list>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* A list that holds weak pointers to objects of type `T`. Null pointers are not
* considered to be in the list.
*
* The list is not thread-safe! The caller is responsible for synchronization.
*/
template <typename T>
class WeakList {
public:
/**
* Call the given function for every element in the list, ensuring the element
* is not destroyed for the duration of the call. Elements are visited in the
* order they were inserted.
*
* As a side effect, any null pointers in the underlying list (corresponding
* to destroyed elements) will be removed during iteration.
*/
template <typename Fn>
void forEach(Fn&& fn) const {
for (auto it = ptrs_.begin(); it != ptrs_.end();) {
if (auto ptr = it->lock()) {
fn(*ptr);
++it;
} else {
it = ptrs_.erase(it);
}
}
}
/**
* Returns the number of (non-null) elements in the list. The count will only
* remain accurate as long as the list is not modified and elements are
* not destroyed.
*
* As a side effect, any null pointers in the underlying list (corresponding
* to destroyed elements) will be removed during this method.
*/
size_t size() const {
size_t count{0};
forEach([&count](const auto&) { ++count; });
return count;
}
/**
* Returns true if there are no elements in the list.
*
* As a side effect, any null pointers in the underlying list (corresponding
* to destroyed elements) will be removed during this method.
*/
bool empty() const {
return !size();
}
/**
* Inserts an element into the list.
*/
void insert(std::weak_ptr<T> ptr) {
ptrs_.push_back(ptr);
}
private:
mutable std::list<std::weak_ptr<T>> ptrs_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,62 @@
/*
* 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. The call is not required if
* didFailWithError was called instead.
*/
virtual void didClose() = 0;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,50 @@
/*
* 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 <folly/dynamic.h>
#include <folly/json.h>
#include <folly/json_pointer.h>
#include <gmock/gmock.h>
#include "FollyDynamicMatchers.h"
namespace facebook::folly_dynamic_matchers_utils {
std::string as_string(std::string value) {
return value;
}
std::string as_string(folly::dynamic value) {
return value.asString();
}
std::string explain_error(
folly::dynamic::json_pointer_resolution_error<folly::dynamic const> error) {
using err_code = folly::dynamic::json_pointer_resolution_error_code;
switch (error.error_code) {
case err_code::key_not_found:
return "key not found";
case err_code::index_out_of_bounds:
return "index out of bounds";
case err_code::append_requested:
return "append requested";
case err_code::index_not_numeric:
return "array index is not numeric";
case err_code::index_has_leading_zero:
return "leading zero not allowed when indexing arrays";
case err_code::element_not_object_or_array:
return "element is neither an object nor an array";
case err_code::json_pointer_out_of_bounds:
return "JSON pointer out of bounds";
case err_code::other:
return "unknown error";
default:
assert(false && "unhandled error code");
return "<unhandled error code>";
}
}
} // namespace facebook::folly_dynamic_matchers_utils

View File

@@ -0,0 +1,80 @@
/*
* 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 <folly/dynamic.h>
#include <folly/json.h>
#include <folly/json_pointer.h>
#include <gmock/gmock.h>
namespace facebook {
namespace folly_dynamic_matchers_utils {
std::string as_string(std::string value);
std::string as_string(folly::dynamic value);
std::string explain_error(
folly::dynamic::json_pointer_resolution_error<folly::dynamic const> error);
} // namespace folly_dynamic_matchers_utils
// GTest / GMock compatible matchers for `folly::dynamic` values.
/**
* Parses a JSON string into a folly::dynamic, then matches it against the
* given matcher.
*/
MATCHER_P(
JsonParsed,
innerMatcher,
std::string{"parsed as JSON "} +
testing::DescribeMatcher<folly::dynamic>(innerMatcher, negation)) {
using namespace ::testing;
using namespace folly_dynamic_matchers_utils;
const auto& json = arg;
folly::dynamic parsed = folly::parseJson(as_string(json));
return ExplainMatchResult(innerMatcher, parsed, result_listener);
}
/**
* Given a folly::dynamic argument, asserts that it is deeply equal to the
* result of parsing the given JSON string.
*/
MATCHER_P(
JsonEq,
expected,
std::string{"deeply equals "} +
folly::toPrettyJson(folly::parseJson(expected))) {
using namespace ::testing;
return ExplainMatchResult(
JsonParsed(Eq(folly::parseJson(expected))), arg, result_listener);
}
/**
* A higher-order matcher that applies an inner matcher to the value at a
* particular JSON Pointer location within a folly::dynamic.
*/
MATCHER_P2(
AtJsonPtr,
jsonPointer,
innerMatcher,
std::string{"value at "} + jsonPointer + " " +
testing::DescribeMatcher<folly::dynamic>(innerMatcher, negation)) {
using namespace ::testing;
using namespace folly_dynamic_matchers_utils;
auto resolved_ptr = arg.try_get_ptr(folly::json_pointer::parse(jsonPointer));
if (resolved_ptr.hasValue()) {
return ExplainMatchResult(
innerMatcher, *resolved_ptr.value().value, result_listener);
}
*result_listener << "has no value at " << jsonPointer << " because of error: "
<< explain_error(resolved_ptr.error());
return false;
}
} // namespace facebook

View File

@@ -0,0 +1,163 @@
/*
* 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 <folly/executors/ScheduledExecutor.h>
#include <gmock/gmock.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/InspectorPackagerConnection.h>
#include <jsinspector-modern/ReactCdp.h>
#include <chrono>
#include <functional>
#include <memory>
#include <string>
// Configurable mocks of various interfaces required by the inspector API.
namespace facebook::react::jsinspector_modern {
class MockWebSocket : public IWebSocket {
public:
MockWebSocket(
const std::string& url,
std::weak_ptr<IWebSocketDelegate> delegate)
: url{url}, delegate{delegate} {
EXPECT_TRUE(this->delegate.lock())
<< "Delegate should exist when provided to createWebSocket";
}
const std::string url;
std::weak_ptr<IWebSocketDelegate> delegate;
/**
* Convenience method to access the delegate from tests.
* \pre The delegate has not been destroyed.
*/
IWebSocketDelegate& getDelegate() {
auto delegateStrong = this->delegate.lock();
EXPECT_TRUE(delegateStrong);
return *delegateStrong;
}
// IWebSocket methods
MOCK_METHOD(void, send, (std::string_view message), (override));
};
class MockRemoteConnection : public IRemoteConnection {
public:
MockRemoteConnection() = default;
// IRemoteConnection methods
MOCK_METHOD(void, onMessage, (std::string message), (override));
MOCK_METHOD(void, onDisconnect, (), (override));
};
class MockLocalConnection : public ILocalConnection {
public:
explicit MockLocalConnection(
std::unique_ptr<IRemoteConnection> remoteConnection)
: remoteConnection_{std::move(remoteConnection)} {}
IRemoteConnection& getRemoteConnection() {
return *remoteConnection_;
}
std::unique_ptr<IRemoteConnection> dangerouslyReleaseRemoteConnection() {
return std::move(remoteConnection_);
}
// ILocalConnection methods
MOCK_METHOD(void, sendMessage, (std::string message), (override));
MOCK_METHOD(void, disconnect, (), (override));
private:
std::unique_ptr<IRemoteConnection> remoteConnection_;
};
class MockInspectorPackagerConnectionDelegate
: public InspectorPackagerConnectionDelegate {
public:
explicit MockInspectorPackagerConnectionDelegate(folly::Executor& executor)
: executor_(executor) {
using namespace testing;
ON_CALL(*this, scheduleCallback(_, _))
.WillByDefault(Invoke<>([this](auto callback, auto delay) {
if (auto scheduledExecutor =
dynamic_cast<folly::ScheduledExecutor*>(&executor_)) {
scheduledExecutor->scheduleAt(
callback, scheduledExecutor->now() + delay);
} else {
executor_.add(callback);
}
}));
}
// InspectorPackagerConnectionDelegate methods
MOCK_METHOD(
std::unique_ptr<IWebSocket>,
connectWebSocket,
(const std::string& url, std::weak_ptr<IWebSocketDelegate> delegate),
(override));
MOCK_METHOD(
void,
scheduleCallback,
(std::function<void(void)> callback, std::chrono::milliseconds delayMs),
(override));
private:
folly::Executor& executor_;
};
class MockPageTargetDelegate : public PageTargetDelegate {
public:
// PageTargetDelegate methods
MOCK_METHOD(void, onReload, (const PageReloadRequest& request), (override));
};
class MockInstanceTargetDelegate : public InstanceTargetDelegate {};
class MockRuntimeTargetDelegate : public RuntimeTargetDelegate {
public:
// RuntimeTargetDelegate methods
MOCK_METHOD(
std::unique_ptr<RuntimeAgentDelegate>,
createAgentDelegate,
(FrontendChannel channel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription&),
(override));
};
class MockRuntimeAgentDelegate : public RuntimeAgentDelegate {
public:
inline MockRuntimeAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>,
const ExecutionContextDescription& executionContextDescription)
: frontendChannel(std::move(frontendChannel)),
sessionState(sessionState),
executionContextDescription(executionContextDescription) {}
// RuntimeAgentDelegate methods
MOCK_METHOD(
bool,
handleRequest,
(const cdp::PreparsedRequest& req),
(override));
const FrontendChannel frontendChannel;
SessionState& sessionState;
const ExecutionContextDescription executionContextDescription;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,576 @@
/*
* 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 <folly/Format.h>
#include <folly/dynamic.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <folly/json.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/PageTarget.h>
#include <memory>
#include "FollyDynamicMatchers.h"
#include "InspectorMocks.h"
#include "UniquePtrFactory.h"
#include "engines/JsiIntegrationTestGenericEngineAdapter.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
using namespace ::testing;
using folly::sformat;
namespace facebook::react::jsinspector_modern {
namespace {
/**
* A text fixture class for the integration between the modern RN CDP backend
* and a JSI engine, mocking out the rest of RN. For simplicity, everything is
* single-threaded and "async" work is actually done through a queued immediate
* executor ( = run immediately and finish all queued sub-tasks before
* returning).
*
* The main limitation of the simpler threading model is that we can't cover
* breakpoints etc - since pausing during JS execution would prevent the test
* from making progress. Such functionality is better suited for a full RN+CDP
* integration test (using RN's own thread management) as well as for each
* engine's unit tests.
*
* \tparam EngineAdapter An adapter class that implements RuntimeTargetDelegate
* for a particular engine, plus exposes access to a RuntimeExecutor (based on
* the provided folly::Executor) and the corresponding jsi::Runtime.
*/
template <typename EngineAdapter>
class JsiIntegrationPortableTest : public Test, private PageTargetDelegate {
folly::QueuedImmediateExecutor immediateExecutor_;
protected:
JsiIntegrationPortableTest() : engineAdapter_{immediateExecutor_} {
instance_ = &page_->registerInstance(instanceTargetDelegate_);
runtimeTarget_ = &instance_->registerRuntime(
*engineAdapter_, engineAdapter_->getRuntimeExecutor());
}
~JsiIntegrationPortableTest() override {
toPage_.reset();
if (runtimeTarget_) {
EXPECT_TRUE(instance_);
instance_->unregisterRuntime(*runtimeTarget_);
runtimeTarget_ = nullptr;
}
if (instance_) {
page_->unregisterInstance(*instance_);
instance_ = nullptr;
}
}
void connect() {
ASSERT_FALSE(toPage_) << "Can only connect once in a JSI integration test.";
toPage_ = page_->connect(
remoteConnections_.make_unique(),
{.integrationName = "JsiIntegrationTest"});
// We'll always get an onDisconnect call when we tear
// down the test. Expect it in order to satisfy the strict mock.
EXPECT_CALL(*remoteConnections_[0], onDisconnect());
}
void reload() {
if (runtimeTarget_) {
ASSERT_TRUE(instance_);
instance_->unregisterRuntime(*runtimeTarget_);
runtimeTarget_ = nullptr;
}
if (instance_) {
page_->unregisterInstance(*instance_);
instance_ = nullptr;
}
// Recreate the engine (e.g. to wipe any state in the inner jsi::Runtime)
engineAdapter_.emplace(immediateExecutor_);
instance_ = &page_->registerInstance(instanceTargetDelegate_);
runtimeTarget_ = &instance_->registerRuntime(
*engineAdapter_, engineAdapter_->getRuntimeExecutor());
}
MockRemoteConnection& fromPage() {
assert(toPage_);
return *remoteConnections_[0];
}
VoidExecutor inspectorExecutor_ = [this](auto callback) {
immediateExecutor_.add(callback);
};
jsi::Value eval(std::string_view code) {
return engineAdapter_->getRuntime().evaluateJavaScript(
std::make_shared<jsi::StringBuffer>(std::string(code)), "<eval>");
}
/**
* Expect a message matching the provided gmock \c matcher and return a holder
* that will eventually contain the parsed JSON payload.
*/
template <typename Matcher>
std::shared_ptr<const std::optional<folly::dynamic>> expectMessageFromPage(
Matcher&& matcher) {
std::shared_ptr result =
std::make_shared<std::optional<folly::dynamic>>(std::nullopt);
EXPECT_CALL(fromPage(), onMessage(matcher))
.WillOnce(
([result](auto message) { *result = folly::parseJson(message); }))
.RetiresOnSaturation();
return result;
}
std::shared_ptr<PageTarget> page_ =
PageTarget::create(*this, inspectorExecutor_);
InstanceTarget* instance_{};
RuntimeTarget* runtimeTarget_{};
MockInstanceTargetDelegate instanceTargetDelegate_;
std::optional<EngineAdapter> engineAdapter_;
private:
UniquePtrFactory<StrictMock<MockRemoteConnection>> remoteConnections_;
protected:
// NOTE: Needs to be destroyed before page_.
std::unique_ptr<ILocalConnection> toPage_;
private:
// PageTargetDelegate methods
void onReload(const PageReloadRequest& request) override {
(void)request;
reload();
}
};
} // namespace
////////////////////////////////////////////////////////////////////////////////
// Some tests are specific to Hermes's CDP capabilities and some are not.
// We'll use JsiIntegrationHermesTest as an alias for Hermes-specific tests
// and JsiIntegrationPortableTest for the engine-agnostic ones.
/**
* The list of engine adapters for which engine-agnostic tests should pass.
*/
using AllEngines = Types<
JsiIntegrationTestHermesEngineAdapter,
JsiIntegrationTestGenericEngineAdapter>;
using AllHermesVariants = Types<JsiIntegrationTestHermesEngineAdapter>;
TYPED_TEST_SUITE(JsiIntegrationPortableTest, AllEngines);
template <typename EngineAdapter>
using JsiIntegrationHermesTest = JsiIntegrationPortableTest<EngineAdapter>;
TYPED_TEST_SUITE(JsiIntegrationHermesTest, AllHermesVariants);
////////////////////////////////////////////////////////////////////////////////
TYPED_TEST(JsiIntegrationPortableTest, ConnectWithoutCrashing) {
this->connect();
}
TYPED_TEST(JsiIntegrationPortableTest, ErrorOnUnknownMethod) {
this->connect();
this->expectMessageFromPage(
JsonParsed(AllOf(AtJsonPtr("/id", 1), AtJsonPtr("/error/code", -32601))));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Foobar.unknownMethod"
})");
}
TYPED_TEST(JsiIntegrationPortableTest, ExecutionContextNotifications) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextCreated",
"params": {
"context": {
"id": 1,
"origin": "",
"name": "main"
}
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextDestroyed",
"params": {
"executionContextId": 1
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextCreated",
"params": {
"context": {
"id": 2,
"origin": "",
"name": "main"
}
}
})"));
// Simulate a reload triggered by the app (not by the debugger).
this->reload();
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextDestroyed",
"params": {
"executionContextId": 2
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextCreated",
"params": {
"context": {
"id": 3,
"origin": "",
"name": "main"
}
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Page.reload"
})");
}
TYPED_TEST(JsiIntegrationPortableTest, AddBinding) {
this->connect();
InSequence s;
auto executionContextInfo = this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
auto executionContextId =
executionContextInfo->value()["params"]["context"]["id"];
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.bindingCalled"),
AtJsonPtr("/params/name", "foo"),
AtJsonPtr("/params/payload", "bar"),
AtJsonPtr("/params/executionContextId", executionContextId))));
this->eval("globalThis.foo('bar');");
}
TYPED_TEST(JsiIntegrationPortableTest, AddedBindingSurvivesReload) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
this->reload();
// Get the new context ID by sending Runtime.enable now.
auto executionContextInfo = this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
auto executionContextId =
executionContextInfo->value()["params"]["context"]["id"];
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.bindingCalled"),
AtJsonPtr("/params/name", "foo"),
AtJsonPtr("/params/payload", "bar"),
AtJsonPtr("/params/executionContextId", executionContextId))));
this->eval("globalThis.foo('bar');");
}
TYPED_TEST(JsiIntegrationPortableTest, RemovedBindingRemainsInstalled) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.removeBinding",
"params": {"name": "foo"}
})");
this->eval("globalThis.foo('bar');");
}
TYPED_TEST(JsiIntegrationPortableTest, RemovedBindingDoesNotSurviveReload) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.removeBinding",
"params": {"name": "foo"}
})");
this->reload();
EXPECT_TRUE(this->eval("typeof globalThis.foo === 'undefined'").getBool());
}
TYPED_TEST(JsiIntegrationPortableTest, AddBindingClobbersExistingProperty) {
this->connect();
InSequence s;
this->eval(R"(
globalThis.foo = 'clobbered value';
)");
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.bindingCalled"),
AtJsonPtr("/params/name", "foo"),
AtJsonPtr("/params/payload", "bar"))));
this->eval("globalThis.foo('bar');");
}
TYPED_TEST(JsiIntegrationPortableTest, ExceptionDuringAddBindingIsIgnored) {
this->connect();
InSequence s;
this->eval(R"(
Object.defineProperty(globalThis, 'foo', {
get: function () { return 42; },
set: function () { throw new Error('nope'); },
});
)");
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
EXPECT_TRUE(this->eval("globalThis.foo === 42").getBool());
}
////////////////////////////////////////////////////////////////////////////////
TYPED_TEST(JsiIntegrationHermesTest, EvaluateExpression) {
this->connect();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {
"result": {
"type": "number",
"value": 42
}
}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": "42"}
})");
}
TYPED_TEST(JsiIntegrationHermesTest, EvaluateExpressionInExecutionContext) {
this->connect();
InSequence s;
auto executionContextInfo = this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
auto executionContextId =
executionContextInfo->value()["params"]["context"]["id"].getInt();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {
"result": {
"type": "number",
"value": 42
}
}
})"));
this->toPage_->sendMessage(sformat(
R"({{
"id": 1,
"method": "Runtime.evaluate",
"params": {{"expression": "42", "contextId": {0}}}
}})",
std::to_string(executionContextId)));
// Silence notifications about execution contexts.
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.disable"
})");
this->reload();
// Now the old execution context is stale.
this->expectMessageFromPage(
JsonParsed(AllOf(AtJsonPtr("/id", 3), AtJsonPtr("/error/code", -32000))));
this->toPage_->sendMessage(sformat(
R"({{
"id": 3,
"method": "Runtime.evaluate",
"params": {{"expression": "10000", "contextId": {0}}}
}})",
std::to_string(executionContextId)));
}
TYPED_TEST(JsiIntegrationHermesTest, ResolveBreakpointAfterReload) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonParsed(AtJsonPtr("/id", 1)));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Debugger.setBreakpointByUrl",
"params": {"lineNumber": 2, "url": "breakpointTest.js"}
})");
this->reload();
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Debugger.enable"
})");
auto scriptInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Debugger.scriptParsed"),
AtJsonPtr("/params/url", "breakpointTest.js"))));
auto breakpointInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Debugger.breakpointResolved"),
AtJsonPtr("/params/location/lineNumber", 2))));
this->eval(R"( // line 0
globalThis.foo = function() { // line 1
Date.now(); // line 2
};
//# sourceURL=breakpointTest.js
)");
EXPECT_EQ(
breakpointInfo->value()["params"]["location"]["scriptId"],
scriptInfo->value()["params"]["scriptId"]);
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,517 @@
/*
* 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 <folly/dynamic.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <folly/json.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/PageTarget.h>
#include <memory>
#include "FollyDynamicMatchers.h"
#include "InspectorMocks.h"
#include "UniquePtrFactory.h"
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
namespace {
class PageTargetTest : public Test {
folly::QueuedImmediateExecutor immediateExecutor_;
protected:
PageTargetTest() {
EXPECT_CALL(runtimeTargetDelegate_, createAgentDelegate(_, _, _, _))
.WillRepeatedly(runtimeAgentDelegates_.lazily_make_unique<
FrontendChannel,
SessionState&,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>,
const ExecutionContextDescription&>());
}
void connect() {
ASSERT_FALSE(toPage_) << "Can only connect once in a PageTargetTest.";
toPage_ = page_->connect(
remoteConnections_.make_unique(),
{.integrationName = "PageTargetTest"});
// We'll always get an onDisconnect call when we tear
// down the test. Expect it in order to satisfy the strict mock.
EXPECT_CALL(*remoteConnections_[0], onDisconnect());
}
MockPageTargetDelegate pageTargetDelegate_;
MockRemoteConnection& fromPage() {
assert(toPage_);
return *remoteConnections_[0];
}
VoidExecutor inspectorExecutor_ = [this](auto callback) {
immediateExecutor_.add(callback);
};
std::shared_ptr<PageTarget> page_ =
PageTarget::create(pageTargetDelegate_, inspectorExecutor_);
MockInstanceTargetDelegate instanceTargetDelegate_;
MockRuntimeTargetDelegate runtimeTargetDelegate_;
// We don't have access to a jsi::Runtime in these tests, so just use an
// executor that never runs the scheduled callbacks.
RuntimeExecutor runtimeExecutor_ = [](auto) {};
UniquePtrFactory<StrictMock<MockRuntimeAgentDelegate>> runtimeAgentDelegates_;
private:
UniquePtrFactory<StrictMock<MockRemoteConnection>> remoteConnections_;
protected:
// NOTE: Needs to be destroyed before page_.
std::unique_ptr<ILocalConnection> toPage_;
};
/**
* Simplified test harness focused on sending messages to and from a PageTarget.
*/
class PageTargetProtocolTest : public PageTargetTest {
public:
PageTargetProtocolTest() {
connect();
}
private:
// Protocol tests shouldn't manually call connect()
using PageTargetTest::connect;
};
} // namespace
TEST_F(PageTargetProtocolTest, UnrecognizedMethod) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32601)), AtJsonPtr("/id", Eq(1))))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "SomeUnrecognizedMethod",
"params": [1, 2]
})");
}
TEST_F(PageTargetProtocolTest, TypeErrorInMethodName) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32600)),
AtJsonPtr("/id", Eq(nullptr))))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": 42,
"params": [1, 2]
})");
}
TEST_F(PageTargetProtocolTest, MissingId) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32600)),
AtJsonPtr("/id", Eq(nullptr))))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"method": "SomeUnrecognizedMethod",
"params": [1, 2]
})");
}
TEST_F(PageTargetProtocolTest, MalformedJson) {
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/error/code", Eq(-32700)),
AtJsonPtr("/id", Eq(nullptr))))))
.RetiresOnSaturation();
toPage_->sendMessage("{");
}
TEST_F(PageTargetProtocolTest, InjectLogsToIdentifyBackend) {
InSequence s;
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Log.entryAdded"),
AtJsonPtr("/params/entry", Not(IsEmpty()))))))
.Times(2)
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Log.enable"
})");
}
TEST_F(PageTargetProtocolTest, PageReloadMethod) {
InSequence s;
EXPECT_CALL(
pageTargetDelegate_,
onReload(Eq(PageTargetDelegate::PageReloadRequest{
.ignoreCache = std::nullopt,
.scriptToEvaluateOnLoad = std::nullopt})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Page.reload"
})");
EXPECT_CALL(
pageTargetDelegate_,
onReload(Eq(PageTargetDelegate::PageReloadRequest{
.ignoreCache = true, .scriptToEvaluateOnLoad = "alert('hello');"})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 2,
"method": "Page.reload",
"params": {
"ignoreCache": true,
"scriptToEvaluateOnLoad": "alert('hello');"
}
})");
}
TEST_F(PageTargetProtocolTest, RegisterUnregisterInstanceWithoutEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetTest, ConnectToAlreadyRegisteredInstanceWithoutEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
connect();
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetProtocolTest, RegisterUnregisterInstanceWithEvents) {
InSequence s;
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})")));
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetTest, ConnectToAlreadyRegisteredInstanceWithEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
connect();
InSequence s;
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})")));
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetTest, ConnectToAlreadyRegisteredRuntimeWithEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
connect();
InSequence s;
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(true))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
static constexpr auto kFooResponse = R"({
"id": 1,
"result": {
"fooValue": 42
}
})";
EXPECT_CALL(fromPage(), onMessage(JsonEq(kFooResponse)))
.RetiresOnSaturation();
runtimeAgentDelegates_[0]->frontendChannel(kFooResponse);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetProtocolTest, RuntimeAgentDelegateLifecycle) {
{
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget = instanceTarget.registerRuntime(
runtimeTargetDelegate_, runtimeExecutor_);
EXPECT_TRUE(runtimeAgentDelegates_[0]);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
EXPECT_FALSE(runtimeAgentDelegates_[0]);
{
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget = instanceTarget.registerRuntime(
runtimeTargetDelegate_, runtimeExecutor_);
EXPECT_TRUE(runtimeAgentDelegates_[1]);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
EXPECT_FALSE(runtimeAgentDelegates_[1]);
}
TEST_F(PageTargetProtocolTest, MethodNotHandledByRuntimeAgentDelegate) {
InSequence s;
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(false))
.RetiresOnSaturation();
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetProtocolTest, MethodHandledByRuntimeAgentDelegate) {
InSequence s;
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(true))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
static constexpr auto kFooResponse = R"({
"id": 1,
"result": {
"fooValue": 42
}
})";
EXPECT_CALL(fromPage(), onMessage(JsonEq(kFooResponse)))
.RetiresOnSaturation();
runtimeAgentDelegates_[0]->frontendChannel(kFooResponse);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetProtocolTest, MessageRoutingWhileNoRuntimeAgentDelegate) {
InSequence s;
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(_))
.WillOnce(Return(true))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 2,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
static constexpr auto kFooResponse = R"({
"id": 2,
"result": {
"fooValue": 42
}
})";
EXPECT_CALL(fromPage(), onMessage(JsonEq(kFooResponse)))
.RetiresOnSaturation();
runtimeAgentDelegates_[0]->frontendChannel(kFooResponse);
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
EXPECT_FALSE(runtimeAgentDelegates_[0]);
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 3,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
}
TEST_F(PageTargetProtocolTest, InstanceWithNullRuntimeAgentDelegate) {
InSequence s;
EXPECT_CALL(runtimeTargetDelegate_, createAgentDelegate(_, _, _, _))
.WillRepeatedly(ReturnNull());
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
auto& runtimeTarget =
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
EXPECT_FALSE(runtimeAgentDelegates_[0]);
EXPECT_CALL(
fromPage(), onMessage(JsonParsed(AtJsonPtr("/error/code", Eq(-32601)))))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "CustomRuntimeDomain.Foo",
"params": {
"expression": "42"
}
})");
instanceTarget.unregisterRuntime(runtimeTarget);
page_->unregisterInstance(instanceTarget);
}
TEST_F(PageTargetProtocolTest, RuntimeAgentDelegateHasAccessToSessionState) {
InSequence s;
// Send Runtime.enable before registering the Instance (which in turns creates
// the RuntimeAgentDelegate).
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
EXPECT_CALL(
fromPage(),
onMessage(
JsonParsed(AtJsonPtr("/method", "Runtime.executionContextCreated"))))
.RetiresOnSaturation();
instanceTarget.registerRuntime(runtimeTargetDelegate_, runtimeExecutor_);
ASSERT_TRUE(runtimeAgentDelegates_[0]);
EXPECT_TRUE(runtimeAgentDelegates_[0]->sessionState.isRuntimeDomainEnabled);
// Send Runtime.disable while the RuntimeAgentDelegate exists - it receives
// the message and can also observe the updated state.
EXPECT_CALL(*runtimeAgentDelegates_[0], handleRequest(Eq(cdp::preparse(R"({
"id": 2,
"method": "Runtime.disable"
})"))));
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {}
})")));
toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.disable"
})");
EXPECT_FALSE(runtimeAgentDelegates_[0]->sessionState.isRuntimeDomainEnabled);
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,110 @@
/*
* 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 <functional>
#include <memory>
namespace facebook {
/**
* A factory that creates objects of type T wrapped in unique_ptr, and provides
* non-owning access to those objects. Note that the factory MUST outlive the
* objects it creates.
*
* Example usage:
*
* struct Foo { virtual ~foo() = default; };
* UniquePtrFactory<Foo> objects;
* std::unique_ptr<Foo> object = objects.make_unique();
* assert(objects[0] == object.get());
* object.reset();
* assert(objects[0] == nullptr);
*
* See UniquePtrFactoryTest.cpp for more examples.
*/
template <typename T>
class UniquePtrFactory {
static_assert(
std::has_virtual_destructor_v<T>,
"T must have a virtual destructor");
public:
/**
* Creates a new object of type T, and returns a unique_ptr wrapping it.
*/
template <typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
size_t index = objectPtrs_.size();
auto ptr =
std::make_unique<Facade>(*this, index, std::forward<Args>(args)...);
objectPtrs_.push_back(ptr.get());
return ptr;
}
/**
* Returns a function that can be used to create objects of type T. The
* function may only be used while the factory is alive.
*/
template <typename... Args>
std::function<std::unique_ptr<T>(Args&&...)> lazily_make_unique() {
return [this](Args&&... args) {
return make_unique(std::forward<Args>(args)...);
};
}
/**
* Returns a pointer to the `index`th object created by this factory,
* or nullptr if the object has been destroyed (or not created yet).
*/
T* operator[](size_t index) {
return index >= objectPtrs_.size() ? nullptr : objectPtrs_[index];
}
/**
* Returns a pointer to the `index`th object created by this factory,
* or nullptr if the object has been destroyed (or not created yet).
*/
const T* operator[](size_t index) const {
return index >= objectPtrs_.size() ? nullptr : objectPtrs_[index];
}
/**
* Returns the total number of objects created by this factory, including
* those that have already been destroyed.
*/
size_t objectsVended() const {
return objectPtrs_.size();
}
private:
friend class Facade;
/**
* Extends T to clean up the reference in objectPtrs_ when the object is
* destroyed.
*/
class Facade : public T {
public:
template <typename... Args>
Facade(UniquePtrFactory& container, size_t index, Args&&... args)
: T(std::forward<Args>(args)...),
container_(container),
index_(index) {}
virtual ~Facade() override {
container_.objectPtrs_[index_] = nullptr;
}
UniquePtrFactory& container_;
size_t index_;
};
std::vector<T*> objectPtrs_;
};
} // namespace facebook

View File

@@ -0,0 +1,84 @@
/*
* 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 <gtest/gtest.h>
#include "UniquePtrFactory.h"
using namespace ::testing;
namespace {
struct Foo {
explicit Foo(int v) : value(v) {}
// Required for UniquePtrFactory
virtual ~Foo() = default;
int value{0};
};
} // namespace
namespace facebook {
TEST(UniquePtrFactoryTest, KitchenSink) {
UniquePtrFactory<Foo> fooObjects;
EXPECT_EQ(fooObjects[0], nullptr)
<< "objects should be nullptr before being created";
EXPECT_EQ(fooObjects.objectsVended(), 0);
auto foo0 = fooObjects.make_unique(100);
EXPECT_EQ(foo0.get(), fooObjects[0]);
EXPECT_EQ(fooObjects.objectsVended(), 1);
auto foo1 = fooObjects.make_unique(200);
EXPECT_EQ(foo1.get(), fooObjects[1]);
EXPECT_EQ(fooObjects.objectsVended(), 2);
foo0.reset();
EXPECT_EQ(fooObjects[0], nullptr)
<< "objects should be nullptr after being destroyed";
EXPECT_EQ(fooObjects.objectsVended(), 2)
<< "objectsVended should never decrease";
EXPECT_EQ(foo1.get(), fooObjects[1])
<< "foo1 should not be affected by foo0 being reset";
foo1.reset();
EXPECT_EQ(fooObjects[1], nullptr)
<< "objects should be nullptr after being destroyed";
EXPECT_EQ(fooObjects.objectsVended(), 2);
auto foo2 = fooObjects.make_unique(300);
EXPECT_EQ(foo2.get(), fooObjects[2]);
EXPECT_EQ(fooObjects.objectsVended(), 3);
}
TEST(UniquePtrFactoryTest, LazilyMakeUnique) {
UniquePtrFactory<Foo> fooObjects;
EXPECT_EQ(fooObjects[0], nullptr)
<< "objects should be nullptr before being created";
EXPECT_EQ(fooObjects.objectsVended(), 0);
auto makeFoo = fooObjects.lazily_make_unique<int>();
EXPECT_EQ(fooObjects[0], nullptr)
<< "an object should not be created until makeFoo is called";
EXPECT_EQ(fooObjects.objectsVended(), 0);
auto foo0 = makeFoo(100);
EXPECT_EQ(foo0.get(), fooObjects[0]);
EXPECT_EQ(fooObjects.objectsVended(), 1);
auto foo1 = makeFoo(200);
EXPECT_EQ(foo1.get(), fooObjects[1]);
EXPECT_EQ(fooObjects.objectsVended(), 2);
}
} // namespace facebook

View File

@@ -0,0 +1,91 @@
/*
* 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 <jsinspector-modern/WeakList.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <vector>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
TEST(WeakListTest, Size) {
WeakList<int> list;
EXPECT_EQ(list.size(), 0);
auto p1 = std::make_shared<int>(1);
list.insert(p1);
EXPECT_EQ(list.size(), 1);
auto p2 = std::make_shared<int>(2);
list.insert(p2);
EXPECT_EQ(list.size(), 2);
p1.reset();
EXPECT_EQ(list.size(), 1);
p2.reset();
EXPECT_EQ(list.size(), 0);
}
TEST(WeakListTest, Empty) {
WeakList<int> list;
EXPECT_EQ(list.empty(), true);
auto p1 = std::make_shared<int>(1);
list.insert(p1);
EXPECT_EQ(list.empty(), false);
auto p2 = std::make_shared<int>(2);
list.insert(p2);
EXPECT_EQ(list.empty(), false);
p1.reset();
EXPECT_EQ(list.empty(), false);
p2.reset();
EXPECT_EQ(list.empty(), true);
}
TEST(WeakListTest, ForEach) {
WeakList<int> list;
auto p1 = std::make_shared<int>(1);
list.insert(p1);
auto p2 = std::make_shared<int>(2);
list.insert(p2);
auto p3 = std::make_shared<int>(3);
list.insert(p3);
p2.reset();
std::vector<int> visited;
list.forEach([&visited](const int& value) { visited.push_back(value); });
EXPECT_THAT(visited, ElementsAre(1, 3));
}
TEST(WeakListTest, ElementsAreAliveDuringCallback) {
WeakList<int> list;
auto p1 = std::make_shared<int>(1);
// A separate weak_ptr to observe the lifetime of `p1`.
std::weak_ptr wp1 = p1;
list.insert(p1);
std::vector<int> visited;
list.forEach([&](const int& value) {
p1.reset();
EXPECT_FALSE(wp1.expired());
visited.push_back(value);
});
EXPECT_TRUE(wp1.expired());
EXPECT_THAT(visited, ElementsAre(1));
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,48 @@
/*
* 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 <jsinspector-modern/FallbackRuntimeAgentDelegate.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <hermes/hermes.h>
#include "JsiIntegrationTestGenericEngineAdapter.h"
using facebook::hermes::makeHermesRuntime;
namespace facebook::react::jsinspector_modern {
JsiIntegrationTestGenericEngineAdapter::JsiIntegrationTestGenericEngineAdapter(
folly::Executor& jsExecutor)
: runtime_{hermes::makeHermesRuntime()}, jsExecutor_{jsExecutor} {}
std::unique_ptr<RuntimeAgentDelegate>
JsiIntegrationTestGenericEngineAdapter::createAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>,
const ExecutionContextDescription&) {
return std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>(
new FallbackRuntimeAgentDelegate(
frontendChannel,
sessionState,
"Generic engine (" + runtime_->description() + ")"));
}
jsi::Runtime& JsiIntegrationTestGenericEngineAdapter::getRuntime()
const noexcept {
return *runtime_;
}
RuntimeExecutor JsiIntegrationTestGenericEngineAdapter::getRuntimeExecutor()
const noexcept {
return [&jsExecutor = jsExecutor_, &runtime = getRuntime()](auto fn) {
jsExecutor.add([fn, &runtime]() { fn(runtime); });
};
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,44 @@
/*
* 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 <jsinspector-modern/RuntimeTarget.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <jsi/jsi.h>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* An engine adapter for JsiIntegrationTest that represents a generic
* JSI-compatible engine, with no engine-specific CDP support. Uses Hermes under
* the hood, without Hermes's CDP support.
*/
class JsiIntegrationTestGenericEngineAdapter : public RuntimeTargetDelegate {
public:
explicit JsiIntegrationTestGenericEngineAdapter(folly::Executor& jsExecutor);
virtual std::unique_ptr<RuntimeAgentDelegate> createAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription) override;
jsi::Runtime& getRuntime() const noexcept;
RuntimeExecutor getRuntimeExecutor() const noexcept;
private:
std::unique_ptr<jsi::Runtime> runtime_;
folly::Executor& jsExecutor_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,58 @@
/*
* 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 <folly/executors/QueuedImmediateExecutor.h>
#include <hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.h>
#include "JsiIntegrationTestHermesEngineAdapter.h"
using facebook::hermes::makeHermesRuntime;
namespace facebook::react::jsinspector_modern {
JsiIntegrationTestHermesEngineAdapter::JsiIntegrationTestHermesEngineAdapter(
folly::Executor& jsExecutor)
: runtime_{hermes::makeHermesRuntime()}, jsExecutor_{jsExecutor} {}
std::unique_ptr<RuntimeAgentDelegate>
JsiIntegrationTestHermesEngineAdapter::createAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription) {
return std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>(
new HermesRuntimeAgentDelegate(
frontendChannel,
sessionState,
std::move(previouslyExportedState),
executionContextDescription,
runtime_,
getRuntimeExecutor()));
}
jsi::Runtime& JsiIntegrationTestHermesEngineAdapter::getRuntime()
const noexcept {
return *runtime_;
}
RuntimeExecutor JsiIntegrationTestHermesEngineAdapter::getRuntimeExecutor()
const noexcept {
auto& jsExecutor = jsExecutor_;
return [runtimeWeak = std::weak_ptr(runtime_), &jsExecutor](auto fn) {
jsExecutor.add([runtimeWeak, fn]() {
auto runtime = runtimeWeak.lock();
if (!runtime) {
return;
}
fn(*runtime);
});
};
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,44 @@
/*
* 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 <jsinspector-modern/RuntimeTarget.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <hermes/hermes.h>
#include <jsi/jsi.h>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* An engine adapter for JsiIntegrationTest that uses Hermes (and Hermes's
* CDP support).
*/
class JsiIntegrationTestHermesEngineAdapter : public RuntimeTargetDelegate {
public:
explicit JsiIntegrationTestHermesEngineAdapter(folly::Executor& jsExecutor);
virtual std::unique_ptr<RuntimeAgentDelegate> createAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription) override;
jsi::Runtime& getRuntime() const noexcept;
RuntimeExecutor getRuntimeExecutor() const noexcept;
private:
std::shared_ptr<facebook::hermes::HermesRuntime> runtime_;
folly::Executor& jsExecutor_;
};
} // namespace facebook::react::jsinspector_modern