first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
/*
* 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 <string>
#include <string_view>
namespace facebook::react::jsinspector_modern {
namespace {
// Vendored from Folly
// https://github.com/facebook/folly/blob/v2024.07.08.00/folly/detail/base64_detail/Base64Scalar.h
constexpr char kBase64Charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
constexpr char kBase64URLCharset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
template <bool isURL>
struct Base64ScalarImpl {
static constexpr const char *kCharset = isURL ? kBase64URLCharset : kBase64Charset;
// 0, 1 or 2 bytes
static constexpr char *encodeTail(const char *f, const char *l, char *o)
{
if (f == l) {
return o;
}
std::uint8_t aaab = f[0];
std::uint8_t aaa = aaab >> 2;
*o++ = kCharset[aaa];
// duplicating some tail handling to try to do less jumps
if (l - f == 1) {
std::uint8_t b00 = aaab << 4 & 0x3f;
*o++ = kCharset[b00];
if constexpr (!isURL) {
*o++ = '=';
*o++ = '=';
}
return o;
}
// l - f == 2
std::uint8_t bbcc = f[1];
std::uint8_t bbb = ((aaab << 4) | (bbcc >> 4)) & 0x3f;
std::uint8_t cc0 = (bbcc << 2) & 0x3f;
*o++ = kCharset[bbb];
*o++ = kCharset[cc0];
if constexpr (!isURL) {
*o++ = '=';
}
return o;
}
static constexpr char *encode(const char *f, const char *l, char *o)
{
while ((l - f) >= 3) {
std::uint8_t aaab = f[0];
std::uint8_t bbcc = f[1];
std::uint8_t cddd = f[2];
std::uint8_t aaa = aaab >> 2;
std::uint8_t bbb = ((aaab << 4) | (bbcc >> 4)) & 0x3f;
std::uint8_t ccc = ((bbcc << 2) | (cddd >> 6)) & 0x3f;
std::uint8_t ddd = cddd & 0x3f;
o[0] = kCharset[aaa];
o[1] = kCharset[bbb];
o[2] = kCharset[ccc];
o[3] = kCharset[ddd];
f += 3;
o += 4;
}
return encodeTail(f, l, o);
}
};
// https://github.com/facebook/folly/blob/v2024.07.08.00/folly/detail/base64_detail/Base64Common.h#L24
constexpr std::size_t base64EncodedSize(std::size_t inSize)
{
return ((inSize + 2) / 3) * 4;
}
} // namespace
inline std::string base64Encode(const std::string_view s)
{
std::string res(base64EncodedSize(s.size()), '\0');
Base64ScalarImpl<false>::encode(s.data(), s.data() + s.size(), res.data());
return res;
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,37 @@
# 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)
include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
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 OBJECT ${jsinspector_SRC})
target_merge_so(jsinspector)
target_include_directories(jsinspector PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(jsinspector
folly_runtime
glog
jsinspector_network
jsinspector_tracing
react_featureflags
runtimeexecutor
reactperflogger
react_utils
)
target_compile_reactnative_options(jsinspector PRIVATE)
if(${CMAKE_BUILD_TYPE} MATCHES Debug OR REACT_NATIVE_DEBUG_OPTIMIZED)
target_compile_options(jsinspector PRIVATE
-DREACT_NATIVE_DEBUGGER_ENABLED=1
-DREACT_NATIVE_DEBUGGER_ENABLED_DEVONLY=1
)
endif ()

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, HostTargetDelegate is used by HostTarget to send host-related events to the native platform implementation.
### Target Controller
A private interface exposed by a Target class to its Sessions/Agents. For example, HostTargetController is used by HostAgent to safely access the host's HostTargetDelegate, without exposing HostTarget'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,40 @@
/*
* 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 "ConsoleMessage.h"
#include <chrono>
#include <utility>
namespace facebook::react::jsinspector_modern {
SimpleConsoleMessage::SimpleConsoleMessage(
double timestamp,
ConsoleAPIType type,
std::vector<std::string> args)
: timestamp(timestamp), type(type), args(std::move(args)) {}
SimpleConsoleMessage::SimpleConsoleMessage(
ConsoleAPIType type,
std::vector<std::string> args)
: timestamp(
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
std::chrono::system_clock::now().time_since_epoch())
.count()),
type(type),
args(std::move(args)) {}
ConsoleMessage::ConsoleMessage(
jsi::Runtime& runtime,
SimpleConsoleMessage message)
: timestamp(message.timestamp), type(message.type) {
for (auto& arg : message.args) {
args.emplace_back(jsi::String::createFromUtf8(runtime, arg));
}
}
} // namespace facebook::react::jsinspector_modern

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.
*/
#pragma once
#include "StackTrace.h"
#include <vector>
#include <jsi/jsi.h>
namespace facebook::react::jsinspector_modern {
enum class ConsoleAPIType {
kLog,
kDebug,
kInfo,
kError,
kWarning,
kDir,
kDirXML,
kTable,
kTrace,
kStartGroup,
kStartGroupCollapsed,
kEndGroup,
kClear,
kAssert,
kTimeEnd,
kCount
};
/**
* A simple, text-only console message that can be described without reference
* to any JSI data.
*/
struct SimpleConsoleMessage {
double timestamp;
ConsoleAPIType type;
std::vector<std::string> args;
SimpleConsoleMessage(double timestamp, ConsoleAPIType type, std::vector<std::string> args);
SimpleConsoleMessage(ConsoleAPIType type, std::vector<std::string> args);
SimpleConsoleMessage(const SimpleConsoleMessage &other) = delete;
SimpleConsoleMessage(SimpleConsoleMessage &&other) noexcept = default;
SimpleConsoleMessage &operator=(const SimpleConsoleMessage &other) = delete;
SimpleConsoleMessage &operator=(SimpleConsoleMessage &&other) noexcept = default;
~SimpleConsoleMessage() = default;
};
/**
* A console message made of JSI values and a captured stack trace.
*/
struct ConsoleMessage {
double timestamp;
ConsoleAPIType type;
std::vector<jsi::Value> args;
std::unique_ptr<StackTrace> stackTrace;
ConsoleMessage(
double timestamp,
ConsoleAPIType type,
std::vector<jsi::Value> args,
std::unique_ptr<StackTrace> stackTrace = StackTrace::empty())
: timestamp(timestamp), type(type), args(std::move(args)), stackTrace(std::move(stackTrace))
{
}
ConsoleMessage(jsi::Runtime &runtime, SimpleConsoleMessage message);
ConsoleMessage(const ConsoleMessage &other) = delete;
ConsoleMessage(ConsoleMessage &&other) noexcept = default;
ConsoleMessage &operator=(const ConsoleMessage &other) = delete;
ConsoleMessage &operator=(ConsoleMessage &&other) noexcept = default;
~ConsoleMessage() = default;
};
} // 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 <array>
#include <limits>
namespace facebook::react::jsinspector_modern {
/**
* A statically-sized array with an enum class as the index type.
* Values are value-initialized (i.e. zero-initialized for integral types).
* Requires that the enum class has a kMaxValue member.
*/
template <class IndexType, class ValueType>
requires std::is_enum_v<IndexType> && std::is_same_v<std::underlying_type_t<IndexType>, int> &&
requires { IndexType::kMaxValue; } && (static_cast<int>(IndexType::kMaxValue) < std::numeric_limits<int>::max())
class EnumArray {
public:
constexpr ValueType &operator[](IndexType i)
{
return array_[static_cast<int>(i)];
}
constexpr const ValueType &operator[](IndexType i) const
{
return array_[static_cast<int>(i)];
}
constexpr int size() const
{
return size_;
}
private:
constexpr static int size_ = static_cast<int>(IndexType::kMaxValue) + 1;
std::array<ValueType, size_> array_{};
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,65 @@
/*
* 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 {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-const-variable"
template <class>
inline constexpr bool always_false_v = false;
#pragma clang diagnostic pop
} // 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_);
}
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,100 @@
/*
* 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 <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,72 @@
/*
* 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>
#include <utility>
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_(std::move(frontendChannel)),
engineDescription_(std::move(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_(
cdp::jsonNotification(
"Log.entryAdded",
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 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.
* \param text The text to send.
*/
void sendWarningLogEntry(std::string_view text);
FrontendChannel frontendChannel_;
std::string engineDescription_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,67 @@
/*
* 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 "FallbackRuntimeTargetDelegate.h"
#include "FallbackRuntimeAgentDelegate.h"
namespace facebook::react::jsinspector_modern {
FallbackRuntimeTargetDelegate::FallbackRuntimeTargetDelegate(
std::string engineDescription)
: engineDescription_{std::move(engineDescription)} {}
std::unique_ptr<RuntimeAgentDelegate>
FallbackRuntimeTargetDelegate::createAgentDelegate(
FrontendChannel channel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
/*previouslyExportedState*/,
const ExecutionContextDescription& /*executionContextDescription*/,
RuntimeExecutor /*runtimeExecutor*/) {
return std::make_unique<jsinspector_modern::FallbackRuntimeAgentDelegate>(
std::move(channel), sessionState, engineDescription_);
}
void FallbackRuntimeTargetDelegate::addConsoleMessage(
jsi::Runtime& /*unused*/,
ConsoleMessage /*unused*/) {
// TODO: Best-effort printing (without RemoteObjects)
}
bool FallbackRuntimeTargetDelegate::supportsConsole() const {
return false;
}
std::unique_ptr<StackTrace> FallbackRuntimeTargetDelegate::captureStackTrace(
jsi::Runtime& /*runtime*/,
size_t /*framesToSkip*/
) {
// TODO: Parse a JS `Error().stack` as a fallback
return std::make_unique<StackTrace>();
}
void FallbackRuntimeTargetDelegate::enableSamplingProfiler() {
// no-op
};
void FallbackRuntimeTargetDelegate::disableSamplingProfiler() {
// no-op
};
tracing::RuntimeSamplingProfile
FallbackRuntimeTargetDelegate::collectSamplingProfile() {
throw std::logic_error(
"Sampling Profiler capabilities are not supported for Runtime fallback");
}
std::optional<folly::dynamic>
FallbackRuntimeTargetDelegate::serializeStackTrace(
const StackTrace& /*stackTrace*/) {
return std::nullopt;
}
} // 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 "InspectorInterfaces.h"
#include "RuntimeTarget.h"
#include "SessionState.h"
#include <string>
namespace facebook::react::jsinspector_modern {
/**
* A RuntimeTargetDelegate that stubs out debugging functionality for a
* JavaScript runtime that does not natively support debugging.
*/
class FallbackRuntimeTargetDelegate : public RuntimeTargetDelegate {
public:
explicit FallbackRuntimeTargetDelegate(std::string engineDescription);
std::unique_ptr<RuntimeAgentDelegate> createAgentDelegate(
FrontendChannel channel,
SessionState &sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState> previouslyExportedState,
const ExecutionContextDescription &executionContextDescription,
RuntimeExecutor runtimeExecutor) override;
void addConsoleMessage(jsi::Runtime &runtime, ConsoleMessage message) override;
bool supportsConsole() const override;
std::unique_ptr<StackTrace> captureStackTrace(jsi::Runtime &runtime, size_t framesToSkip) override;
void enableSamplingProfiler() override;
void disableSamplingProfiler() override;
tracing::RuntimeSamplingProfile collectSamplingProfile() override;
std::optional<folly::dynamic> serializeStackTrace(const StackTrace &stackTrace) override;
private:
std::string engineDescription_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,38 @@
/*
* 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.
*/
/**
* `console` methods that have no behaviour other than emitting a
* Runtime.consoleAPICalled message.
*/
// console.clear
FORWARDING_CONSOLE_METHOD(clear, ConsoleAPIType::kClear)
// console.debug
FORWARDING_CONSOLE_METHOD(debug, ConsoleAPIType::kDebug)
// console.dir
FORWARDING_CONSOLE_METHOD(dir, ConsoleAPIType::kDir)
// console.dirxml
FORWARDING_CONSOLE_METHOD(dirxml, ConsoleAPIType::kDirXML)
// console.error
FORWARDING_CONSOLE_METHOD(error, ConsoleAPIType::kError)
// console.group
FORWARDING_CONSOLE_METHOD(group, ConsoleAPIType::kStartGroup)
// console.groupCollapsed
FORWARDING_CONSOLE_METHOD(groupCollapsed, ConsoleAPIType::kStartGroupCollapsed)
// console.groupEnd
FORWARDING_CONSOLE_METHOD(groupEnd, ConsoleAPIType::kEndGroup)
// console.info
FORWARDING_CONSOLE_METHOD(info, ConsoleAPIType::kInfo)
// console.log
FORWARDING_CONSOLE_METHOD(log, ConsoleAPIType::kLog)
// console.table
FORWARDING_CONSOLE_METHOD(table, ConsoleAPIType::kTable)
// console.trace
FORWARDING_CONSOLE_METHOD(trace, ConsoleAPIType::kTrace)
// console.warn
FORWARDING_CONSOLE_METHOD(warn, ConsoleAPIType::kWarning)

View File

@@ -0,0 +1,568 @@
/*
* 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 "HostAgent.h"
#include "InstanceAgent.h"
#ifdef REACT_NATIVE_DEBUGGER_ENABLED
#include "InspectorFlags.h"
#include "InspectorInterfaces.h"
#include "NetworkIOAgent.h"
#include "SessionState.h"
#include "TracingAgent.h"
#endif // REACT_NATIVE_DEBUGGER_ENABLED
#include <folly/dynamic.h>
#include <folly/json.h>
#include <jsinspector-modern/cdp/CdpJson.h>
#include <chrono>
#include <functional>
#include <string_view>
using namespace std::chrono;
using namespace std::literals::string_view_literals;
namespace facebook::react::jsinspector_modern {
#ifdef REACT_NATIVE_DEBUGGER_ENABLED
#define ANSI_WEIGHT_BOLD "\x1B[1m"
#define ANSI_WEIGHT_RESET "\x1B[22m"
#define ANSI_COLOR_BG_YELLOW "\x1B[48;2;253;247;231m\x1B[30m"
class HostAgent::Impl final {
public:
explicit Impl(
HostAgent& /*hostAgent*/,
const FrontendChannel& frontendChannel,
HostTargetController& targetController,
HostTargetMetadata hostMetadata,
SessionState& sessionState,
VoidExecutor executor)
: frontendChannel_(frontendChannel),
targetController_(targetController),
hostMetadata_(std::move(hostMetadata)),
sessionState_(sessionState),
networkIOAgent_(NetworkIOAgent(frontendChannel, std::move(executor))),
tracingAgent_(
TracingAgent(frontendChannel, sessionState, targetController)) {}
~Impl() {
if (isPausedInDebuggerOverlayVisible_) {
// In case of a non-graceful shutdown of the session, ensure we clean up
// the "paused on debugger" overlay if we've previously asked the
// integrator to display it.
isPausedInDebuggerOverlayVisible_ = false;
if (!targetController_.decrementPauseOverlayCounter()) {
targetController_.getDelegate().onSetPausedInDebuggerMessage({
.message = std::nullopt,
});
}
}
}
private:
struct RequestHandlingState {
bool isFinishedHandlingRequest{false};
bool shouldSendOKResponse{false};
};
RequestHandlingState tryHandleRequest(const cdp::PreparsedRequest& req) {
// 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;
if (fuseboxClientType_ == FuseboxClientType::Fusebox) {
sendFuseboxNotice();
}
// Send a log entry with the integration name.
if (hostMetadata_.integrationName) {
sendInfoLogEntry(
ANSI_COLOR_BG_YELLOW "Debugger integration: " +
*hostMetadata_.integrationName);
}
return {
.isFinishedHandlingRequest = false,
.shouldSendOKResponse = true,
};
}
if (req.method == "Log.disable") {
sessionState_.isLogDomainEnabled = false;
return {
.isFinishedHandlingRequest = false,
.shouldSendOKResponse = true,
};
}
if (req.method == "Runtime.enable") {
sessionState_.isRuntimeDomainEnabled = true;
if (fuseboxClientType_ == FuseboxClientType::Unknown) {
// Since we know the Fusebox frontend sends
// FuseboxClient.setClientMetadata before enabling the Runtime domain,
// we can conclude that we're dealing with some other client.
fuseboxClientType_ = FuseboxClientType::NonFusebox;
sendNonFuseboxNotice();
}
return {
.isFinishedHandlingRequest = false,
.shouldSendOKResponse = true,
};
}
if (req.method == "Runtime.disable") {
sessionState_.isRuntimeDomainEnabled = false;
return {
.isFinishedHandlingRequest = false,
.shouldSendOKResponse = true,
};
}
if (req.method == "Debugger.enable") {
sessionState_.isDebuggerDomainEnabled = true;
return {
.isFinishedHandlingRequest = false,
.shouldSendOKResponse = true,
};
}
if (req.method == "Debugger.disable") {
sessionState_.isDebuggerDomainEnabled = false;
return {
.isFinishedHandlingRequest = false,
.shouldSendOKResponse = true,
};
}
if (InspectorFlags::getInstance().getNetworkInspectionEnabled()) {
if (req.method == "Network.enable") {
auto& inspector = getInspectorInstance();
if (inspector.getSystemState().registeredHostsCount > 1) {
frontendChannel_(
cdp::jsonError(
req.id,
cdp::ErrorCode::InternalError,
"The Network domain is unavailable when multiple React Native hosts are registered."));
return {
.isFinishedHandlingRequest = true,
.shouldSendOKResponse = false,
};
}
sessionState_.isNetworkDomainEnabled = true;
return {
.isFinishedHandlingRequest = false,
.shouldSendOKResponse = true,
};
}
if (req.method == "Network.disable") {
sessionState_.isNetworkDomainEnabled = false;
return {
.isFinishedHandlingRequest = false,
.shouldSendOKResponse = true,
};
}
}
// Methods other than domain enables/disables: handle anything we know how
// to handle, and delegate to the InstanceAgent otherwise. (In some special
// cases we may handle the request *and* delegate to the InstanceAgent for
// some side effect.)
if (req.method == "Page.reload") {
targetController_.getDelegate().onReload({
.ignoreCache =
req.params.isObject() && (req.params.count("ignoreCache") != 0u)
? std::optional(req.params.at("ignoreCache").asBool())
: std::nullopt,
.scriptToEvaluateOnLoad = req.params.isObject() &&
(req.params.count("scriptToEvaluateOnLoad") != 0u)
? std::optional(
req.params.at("scriptToEvaluateOnLoad").asString())
: std::nullopt,
});
return {
.isFinishedHandlingRequest = true,
.shouldSendOKResponse = true,
};
}
if (req.method == "Overlay.setPausedInDebuggerMessage") {
auto message =
req.params.isObject() && (req.params.count("message") != 0u)
? std::optional(req.params.at("message").asString())
: std::nullopt;
if (!isPausedInDebuggerOverlayVisible_ && message.has_value()) {
targetController_.incrementPauseOverlayCounter();
} else if (isPausedInDebuggerOverlayVisible_ && !message.has_value()) {
targetController_.decrementPauseOverlayCounter();
}
isPausedInDebuggerOverlayVisible_ = message.has_value();
targetController_.getDelegate().onSetPausedInDebuggerMessage({
.message = message,
});
return {
.isFinishedHandlingRequest = true,
.shouldSendOKResponse = true,
};
}
if (req.method == "ReactNativeApplication.enable") {
sessionState_.isReactNativeApplicationDomainEnabled = true;
fuseboxClientType_ = FuseboxClientType::Fusebox;
if (sessionState_.isLogDomainEnabled) {
sendFuseboxNotice();
}
frontendChannel_(
cdp::jsonNotification(
"ReactNativeApplication.metadataUpdated",
createHostMetadataPayload(hostMetadata_)));
auto& inspector = getInspectorInstance();
bool isSingleHost = inspector.getSystemState().registeredHostsCount <= 1;
if (!isSingleHost) {
emitSystemStateChanged(isSingleHost);
}
auto stashedTraceRecording =
targetController_.getDelegate()
.unstable_getTraceRecordingThatWillBeEmittedOnInitialization();
if (stashedTraceRecording.has_value()) {
tracingAgent_.emitExternalTraceRecording(
std::move(stashedTraceRecording.value()));
}
return {
.isFinishedHandlingRequest = true,
.shouldSendOKResponse = true,
};
}
if (req.method == "ReactNativeApplication.disable") {
sessionState_.isReactNativeApplicationDomainEnabled = false;
return {
.isFinishedHandlingRequest = true,
.shouldSendOKResponse = true,
};
}
if (req.method == "Runtime.addBinding") {
// @cdp Runtime.addBinding and @cdp Runtime.removeBinding are explicitly
// supported at any time during a session, even while the JS runtime
// hasn't been created yet. For this reason they are handled by the
// HostAgent.
std::string bindingName = req.params["name"].getString();
ExecutionContextSelector contextSelector =
ExecutionContextSelector::all();
if (req.params.count("executionContextId") != 0u) {
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_(
cdp::jsonError(
req.id,
cdp::ErrorCode::InvalidParams,
"Invalid execution context id"));
return {
.isFinishedHandlingRequest = true,
.shouldSendOKResponse = false,
};
}
contextSelector =
ExecutionContextSelector::byId((int32_t)executionContextId);
if (req.params.count("executionContextName") != 0u) {
frontendChannel_(
cdp::jsonError(
req.id,
cdp::ErrorCode::InvalidParams,
"executionContextName is mutually exclusive with executionContextId"));
return {
.isFinishedHandlingRequest = true,
.shouldSendOKResponse = false,
};
}
} else if (req.params.count("executionContextName") != 0u) {
contextSelector = ExecutionContextSelector::byName(
req.params["executionContextName"].getString());
}
sessionState_.subscribedBindings[bindingName].insert(contextSelector);
// We need this request to percolate down to the RuntimeAgent via the
// InstanceAgent. If there isn't a RuntimeAgent, it's OK: the next
// RuntimeAgent will pick up the binding via session state.
return {
.isFinishedHandlingRequest = false,
.shouldSendOKResponse = 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());
// Because of the above, we don't need to pass this request down to the
// RuntimeAgent.
return {
.isFinishedHandlingRequest = true,
.shouldSendOKResponse = true,
};
}
return {
.isFinishedHandlingRequest = false,
.shouldSendOKResponse = false,
};
}
public:
void handleRequest(const cdp::PreparsedRequest& req) {
const RequestHandlingState requestState = tryHandleRequest(req);
if (!requestState.isFinishedHandlingRequest &&
networkIOAgent_.handleRequest(req, targetController_.getDelegate())) {
return;
}
if (!requestState.isFinishedHandlingRequest &&
tracingAgent_.handleRequest(req)) {
return;
}
if (!requestState.isFinishedHandlingRequest && instanceAgent_ &&
instanceAgent_->handleRequest(req)) {
return;
}
if (requestState.shouldSendOKResponse) {
frontendChannel_(cdp::jsonResult(req.id));
return;
}
throw NotImplementedException(req.method);
}
void 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.
frontendChannel_(
cdp::jsonNotification("Runtime.executionContextsCleared"));
}
if (instanceAgent_) {
// TODO: Send Runtime.executionContextCreated here - at the moment we
// expect the runtime to do it for us.
}
}
bool hasFuseboxClientConnected() const {
return fuseboxClientType_ == FuseboxClientType::Fusebox;
}
void emitExternalTraceRecording(
tracing::TraceRecordingState traceRecording) const {
assert(
hasFuseboxClientConnected() &&
"Attempted to emit a trace recording to a non-Fusebox client");
tracingAgent_.emitExternalTraceRecording(std::move(traceRecording));
}
void emitSystemStateChanged(bool isSingleHost) {
frontendChannel_(
cdp::jsonNotification(
"ReactNativeApplication.systemStateChanged",
folly::dynamic::object("isSingleHost", isSingleHost)));
frontendChannel_(cdp::jsonNotification("Network.disable"));
}
private:
enum class FuseboxClientType { Unknown, Fusebox, NonFusebox };
/**
* 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 Host level.
*/
void sendInfoLogEntry(
std::string_view text,
std::initializer_list<std::string_view> args = {}) {
folly::dynamic argsArray = folly::dynamic::array();
for (auto arg : args) {
argsArray.push_back(arg);
}
frontendChannel_(
cdp::jsonNotification(
"Log.entryAdded",
folly::dynamic::object(
"entry",
folly::dynamic::object(
"timestamp",
duration_cast<milliseconds>(
system_clock::now().time_since_epoch())
.count())("source", "other")("level", "info")(
"text", text)("args", std::move(argsArray)))));
}
void sendFuseboxNotice() {
static constexpr auto kFuseboxNotice =
ANSI_COLOR_BG_YELLOW "Welcome to " ANSI_WEIGHT_BOLD
"React Native DevTools" ANSI_WEIGHT_RESET ""sv;
sendInfoLogEntry(kFuseboxNotice);
}
void sendNonFuseboxNotice() {
static constexpr auto kNonFuseboxNotice =
ANSI_COLOR_BG_YELLOW ANSI_WEIGHT_BOLD
"NOTE: " ANSI_WEIGHT_RESET
"You are using an unsupported debugging client. "
"Use the Dev Menu in your app (or type `j` in the Metro terminal) to open React Native DevTools."sv;
std::vector<std::string> args;
args.emplace_back(kNonFuseboxNotice);
sendConsoleMessage({ConsoleAPIType::kInfo, args});
}
/**
* Send a console message to the frontend, or buffer it to be sent later.
*/
void sendConsoleMessage(SimpleConsoleMessage message) {
if (instanceAgent_) {
instanceAgent_->sendConsoleMessage(std::move(message));
} else {
// Will be sent by the InstanceAgent eventually.
sessionState_.pendingSimpleConsoleMessages.emplace_back(
std::move(message));
}
}
FrontendChannel frontendChannel_;
HostTargetController& targetController_;
const HostTargetMetadata hostMetadata_;
std::shared_ptr<InstanceAgent> instanceAgent_;
FuseboxClientType fuseboxClientType_{FuseboxClientType::Unknown};
bool isPausedInDebuggerOverlayVisible_{false};
/**
* 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_;
NetworkIOAgent networkIOAgent_;
TracingAgent tracingAgent_;
};
#else
/**
* A stub for HostAgent when React Native is compiled without debugging
* support.
*/
class HostAgent::Impl final {
public:
explicit Impl(
HostAgent&,
FrontendChannel frontendChannel,
HostTargetController& targetController,
HostTargetMetadata hostMetadata,
SessionState& sessionState,
VoidExecutor executor) {}
void handleRequest(const cdp::PreparsedRequest& req) {}
void setCurrentInstanceAgent(std::shared_ptr<InstanceAgent> agent) {}
bool hasFuseboxClientConnected() const {
return false;
}
void emitExternalTraceRecording(tracing::TraceRecordingState traceRecording) {
}
void emitSystemStateChanged(bool isSingleHost) {}
};
#endif // REACT_NATIVE_DEBUGGER_ENABLED
HostAgent::HostAgent(
const FrontendChannel& frontendChannel,
HostTargetController& targetController,
HostTargetMetadata hostMetadata,
SessionState& sessionState,
VoidExecutor executor)
: impl_(
std::make_unique<Impl>(
*this,
frontendChannel,
targetController,
std::move(hostMetadata),
sessionState,
std::move(executor))) {}
HostAgent::~HostAgent() = default;
void HostAgent::handleRequest(const cdp::PreparsedRequest& req) {
impl_->handleRequest(req);
}
void HostAgent::setCurrentInstanceAgent(
std::shared_ptr<InstanceAgent> instanceAgent) {
impl_->setCurrentInstanceAgent(std::move(instanceAgent));
}
bool HostAgent::hasFuseboxClientConnected() const {
return impl_->hasFuseboxClientConnected();
}
void HostAgent::emitExternalTraceRecording(
tracing::TraceRecordingState traceRecording) const {
impl_->emitExternalTraceRecording(std::move(traceRecording));
}
void HostAgent::emitSystemStateChanged(bool isSingleHost) const {
impl_->emitSystemStateChanged(isSingleHost);
}
#pragma mark - Tracing
HostTracingAgent::HostTracingAgent(tracing::TraceRecordingState& state)
: tracing::TargetTracingAgent(state) {}
void HostTracingAgent::setTracedInstance(InstanceTarget* instanceTarget) {
if (instanceTarget != nullptr) {
instanceTracingAgent_ = instanceTarget->createTracingAgent(state_);
} else {
instanceTracingAgent_ = nullptr;
}
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,119 @@
/*
* 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 "HostTarget.h"
#include <jsinspector-modern/InspectorInterfaces.h>
#include <jsinspector-modern/InstanceAgent.h>
#include <jsinspector-modern/cdp/CdpJson.h>
#include <jsinspector-modern/tracing/TargetTracingAgent.h>
namespace facebook::react::jsinspector_modern {
class InstanceAgent;
/**
* An Agent that handles requests from the Chrome DevTools Protocol for the
* given Host.
* The constructor, destructor and all public methods must be called on the
* same thread, which is also the thread where the associated HostTarget is
* constructed and managed.
*/
class HostAgent final {
public:
/**
* \param frontendChannel A channel used to send responses and events to the
* frontend.
* \param targetController An interface to the HostTarget that this agent is
* attached to. The caller is responsible for ensuring that the
* HostTargetDelegate and underlying HostTarget both outlive the agent.
* \param hostMetadata Metadata about the host that created this agent.
* \param sessionState The state of the session that created this agent.
* \param executor A void executor to be used by async-aware handlers.
*/
HostAgent(
const FrontendChannel &frontendChannel,
HostTargetController &targetController,
HostTargetMetadata hostMetadata,
SessionState &sessionState,
VoidExecutor executor);
HostAgent(const HostAgent &) = delete;
HostAgent(HostAgent &&) = delete;
HostAgent &operator=(const HostAgent &) = delete;
HostAgent &operator=(HostAgent &&) = delete;
~HostAgent();
/**
* 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);
/**
* Returns whether this HostAgent is part of the session that has an active
* Fusebox client connecte, i.e. with Chrome DevTools Frontend fork for React
* Native.
*/
bool hasFuseboxClientConnected() const;
/**
* Emits the trace recording that was captured externally, not via the
* CDP-initiated request.
*/
void emitExternalTraceRecording(tracing::TraceRecordingState traceRecording) const;
/**
* Emits a system state changed event when the number of ReactHost instances
* changes.
*/
void emitSystemStateChanged(bool isSingleHost) const;
private:
// We use the private implementation idiom to ensure this class has the same
// layout regardless of whether REACT_NATIVE_DEBUGGER_ENABLED is defined. The
// net effect is that callers can include HostAgent.h without setting
// HERMES_ENABLE_DEBUGGER one way or the other.
class Impl;
std::unique_ptr<Impl> impl_;
};
#pragma mark - Tracing
/**
* An Agent that handles Tracing events for a particular InstanceTarget.
*
* Lifetime of this agent is bound to the lifetime of the Tracing session -
* HostTargetTraceRecording.
*/
class HostTracingAgent : tracing::TargetTracingAgent {
public:
explicit HostTracingAgent(tracing::TraceRecordingState &state);
/**
* Registers the InstanceTarget with this tracing agent.
*/
void setTracedInstance(InstanceTarget *instanceTarget);
private:
std::shared_ptr<InstanceTracingAgent> instanceTracingAgent_{nullptr};
};
} // namespace facebook::react::jsinspector_modern

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.
*/
#pragma once
namespace facebook::react::jsinspector_modern {
enum class HostCommand {
/** Resumes JavaScript execution. */
DebuggerResume,
/** Steps over the statement. */
DebuggerStepOver
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,405 @@
/*
* 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 "HostTarget.h"
#include "HostAgent.h"
#include "HostTargetTraceRecording.h"
#include "InspectorInterfaces.h"
#include "InspectorUtilities.h"
#include "InstanceTarget.h"
#include "SessionState.h"
#include <jsinspector-modern/InspectorFlags.h>
#include <jsinspector-modern/cdp/CdpJson.h>
#include <folly/dynamic.h>
#include <folly/json.h>
#include <memory>
#include <utility>
namespace facebook::react::jsinspector_modern {
/**
* A Session connected to a HostTarget, passing CDP messages to and from a
* HostAgent which it owns.
*/
class HostTargetSession {
public:
explicit HostTargetSession(
std::unique_ptr<IRemoteConnection> remote,
HostTargetController& targetController,
HostTargetMetadata hostMetadata,
VoidExecutor executor)
: 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));
}
}),
hostAgent_(
frontendChannel_,
targetController,
std::move(hostMetadata),
state_,
std::move(executor)) {}
/**
* Called by CallbackLocalConnection to send a message to this Session's
* Agent.
*/
void operator()(const 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_(
cdp::jsonError(std::nullopt, cdp::ErrorCode::ParseError, e.what()));
return;
} catch (const cdp::TypeError& e) {
frontendChannel_(
cdp::jsonError(
std::nullopt, cdp::ErrorCode::InvalidRequest, e.what()));
return;
}
try {
hostAgent_.handleRequest(request);
}
// Catch exceptions that may arise from accessing dynamic params during
// request handling.
catch (const cdp::TypeError& e) {
frontendChannel_(
cdp::jsonError(request.id, cdp::ErrorCode::InvalidRequest, e.what()));
return;
}
// Catch exceptions for unrecognised or partially implemented CDP methods.
catch (const NotImplementedException& e) {
frontendChannel_(
cdp::jsonError(request.id, cdp::ErrorCode::MethodNotFound, e.what()));
return;
}
}
/**
* Replace the current instance agent inside hostAgent_ 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 != nullptr) {
hostAgent_.setCurrentInstanceAgent(
instance->createAgent(frontendChannel_, state_));
} else {
hostAgent_.setCurrentInstanceAgent(nullptr);
}
}
/**
* Returns whether the ReactNativeApplication CDP domain is enabled.
*
* Chrome DevTools Frontend enables this domain as a client.
*/
bool hasFuseboxClient() const {
return hostAgent_.hasFuseboxClientConnected();
}
void emitTraceRecording(tracing::TraceRecordingState traceRecording) const {
hostAgent_.emitExternalTraceRecording(std::move(traceRecording));
}
private:
// Owned by this instance, but shared (weakly) with the frontend channel
std::shared_ptr<RAIIRemoteConnection> remote_;
FrontendChannel frontendChannel_;
SessionState state_;
// NOTE: hostAgent_ has a raw reference to state_ so must be destroyed first.
HostAgent hostAgent_;
};
/**
* Converts HostCommands to CDP method calls and sends them over a private
* connection to the HostTarget.
*/
class HostCommandSender {
public:
explicit HostCommandSender(HostTarget& target)
: connection_(target.connect(std::make_unique<NullRemoteConnection>())) {}
/**
* Send a \c HostCommand to the HostTarget.
*/
void sendCommand(HostCommand command) {
cdp::RequestId id = makeRequestId();
switch (command) {
case HostCommand::DebuggerResume:
connection_->sendMessage(cdp::jsonRequest(id, "Debugger.resume"));
break;
case HostCommand::DebuggerStepOver:
connection_->sendMessage(cdp::jsonRequest(id, "Debugger.stepOver"));
break;
default:
assert(false && "unknown HostCommand");
}
}
private:
cdp::RequestId makeRequestId() {
return nextRequestId_++;
}
cdp::RequestId nextRequestId_{1};
std::unique_ptr<ILocalConnection> connection_;
};
/**
* Enables the caller to install and subscribe to a named CDP runtime binding
* on the HostTarget via a callback. Note: Per CDP spec, this does not need to
* check if the `Runtime` domain is enabled.
*/
class HostRuntimeBinding {
public:
explicit HostRuntimeBinding(
HostTarget& target,
std::string name,
std::function<void(std::string)> callback)
: connection_(target.connect(
std::make_unique<CallbackRemoteConnection>(
[callback = std::move(callback)](const std::string& message) {
auto parsedMessage = folly::parseJson(message);
// Ignore initial Runtime.addBinding response
if (parsedMessage["id"] == 0 &&
parsedMessage["result"].isObject() &&
parsedMessage["result"].empty()) {
return;
}
// Assert that we only intercept bindingCalled responses
assert(
parsedMessage["method"].asString() ==
"Runtime.bindingCalled");
callback(parsedMessage["params"]["payload"].asString());
}))) {
// Install runtime binding
connection_->sendMessage(
cdp::jsonRequest(
0,
"Runtime.addBinding",
folly::dynamic::object("name", std::move(name))));
}
private:
std::unique_ptr<ILocalConnection> connection_;
};
std::shared_ptr<HostTarget> HostTarget::create(
HostTargetDelegate& delegate,
VoidExecutor executor) {
std::shared_ptr<HostTarget> hostTarget{new HostTarget(delegate)};
hostTarget->setExecutor(std::move(executor));
if (InspectorFlags::getInstance().getPerfIssuesEnabled()) {
hostTarget->installPerfIssuesBinding();
}
return hostTarget;
}
HostTarget::HostTarget(HostTargetDelegate& delegate)
: delegate_(delegate),
executionContextManager_{std::make_shared<ExecutionContextManager>()} {}
std::unique_ptr<ILocalConnection> HostTarget::connect(
std::unique_ptr<IRemoteConnection> connectionToFrontend) {
auto session = std::make_shared<HostTargetSession>(
std::move(connectionToFrontend),
controller_,
delegate_.getMetadata(),
makeVoidExecutor(executorFromThis()));
session->setCurrentInstance(currentInstance_.get());
sessions_.insert(std::weak_ptr(session));
return std::make_unique<CallbackLocalConnection>(
[session](const std::string& message) { (*session)(message); });
}
HostTarget::~HostTarget() {
// HostCommandSender owns a session, so we must release it for the assertion
// below to be valid.
commandSender_.reset();
// HostRuntimeBinding owns a connection, so we must release it for the
// assertion
perfMetricsBinding_.reset();
// Sessions are owned by InspectorPackagerConnection, not by HostTarget, but
// they hold a HostTarget& that we must guarantee is valid.
assert(
sessions_.empty() &&
"HostTargetSession objects must be destroyed before their HostTarget. Did you call getInspectorInstance().removePage()?");
// Trace Recording object (traceRecording_) doesn't create an actual session,
// so we don't need to reset it explicitly here.
}
HostTargetDelegate::~HostTargetDelegate() = default;
InstanceTarget& HostTarget::registerInstance(InstanceTargetDelegate& delegate) {
assert(!currentInstance_ && "Only one instance allowed");
currentInstance_ = InstanceTarget::create(
executionContextManager_, delegate, makeVoidExecutor(executorFromThis()));
sessions_.forEach(
[currentInstance = &*currentInstance_](HostTargetSession& session) {
session.setCurrentInstance(currentInstance);
});
if (traceRecording_) {
// Registers the Instance for tracing, if a Trace is currently being
// recorded.
traceRecording_->setTracedInstance(currentInstance_.get());
}
return *currentInstance_;
}
void HostTarget::unregisterInstance(InstanceTarget& instance) {
assert(
currentInstance_ && currentInstance_.get() == &instance &&
"Invalid unregistration");
sessions_.forEach(
[](HostTargetSession& session) { session.setCurrentInstance(nullptr); });
if (traceRecording_) {
// Unregisters the Instance for tracing, if a Trace is currently being
// recorded.
traceRecording_->setTracedInstance(nullptr);
}
currentInstance_.reset();
}
void HostTarget::sendCommand(HostCommand command) {
executorFromThis()([command](HostTarget& self) {
if (!self.commandSender_) {
self.commandSender_ = std::make_unique<HostCommandSender>(self);
}
self.commandSender_->sendCommand(command);
});
}
void HostTarget::installPerfIssuesBinding() {
perfMonitorUpdateHandler_ =
std::make_unique<PerfMonitorUpdateHandler>(delegate_);
perfMetricsBinding_ = std::make_unique<HostRuntimeBinding>(
*this, // Used immediately
"__react_native_perf_issues_reporter",
[this](const std::string& message) {
perfMonitorUpdateHandler_->handlePerfIssueAdded(message);
});
}
HostTargetController::HostTargetController(HostTarget& target)
: target_(target) {}
HostTargetDelegate& HostTargetController::getDelegate() {
return target_.getDelegate();
}
bool HostTargetController::hasInstance() const {
return target_.hasInstance();
}
void HostTargetController::incrementPauseOverlayCounter() {
++pauseOverlayCounter_;
}
bool HostTargetController::decrementPauseOverlayCounter() {
assert(pauseOverlayCounter_ > 0 && "Pause overlay counter underflow");
return --pauseOverlayCounter_ != 0;
}
namespace {
struct StaticHostTargetMetadata {
std::optional<bool> isProfilingBuild;
std::optional<bool> networkInspectionEnabled;
};
StaticHostTargetMetadata getStaticHostMetadata() {
auto& inspectorFlags = jsinspector_modern::InspectorFlags::getInstance();
return {
.isProfilingBuild = inspectorFlags.getIsProfilingBuild(),
.networkInspectionEnabled = inspectorFlags.getNetworkInspectionEnabled()};
}
} // namespace
folly::dynamic createHostMetadataPayload(const HostTargetMetadata& metadata) {
auto staticMetadata = getStaticHostMetadata();
folly::dynamic result = folly::dynamic::object;
if (metadata.appDisplayName) {
result["appDisplayName"] = metadata.appDisplayName.value();
}
if (metadata.appIdentifier) {
result["appIdentifier"] = metadata.appIdentifier.value();
}
if (metadata.deviceName) {
result["deviceName"] = metadata.deviceName.value();
}
if (metadata.integrationName) {
result["integrationName"] = metadata.integrationName.value();
}
if (metadata.platform) {
result["platform"] = metadata.platform.value();
}
if (metadata.reactNativeVersion) {
result["reactNativeVersion"] = metadata.reactNativeVersion.value();
}
if (staticMetadata.isProfilingBuild) {
result["unstable_isProfilingBuild"] =
staticMetadata.isProfilingBuild.value();
}
if (staticMetadata.networkInspectionEnabled) {
result["unstable_networkInspectionEnabled"] =
staticMetadata.networkInspectionEnabled.value();
}
return result;
}
bool HostTarget::hasActiveSessionWithFuseboxClient() const {
bool hasActiveFuseboxSession = false;
sessions_.forEach([&](HostTargetSession& session) {
hasActiveFuseboxSession |= session.hasFuseboxClient();
});
return hasActiveFuseboxSession;
}
void HostTarget::emitTraceRecordingForFirstFuseboxClient(
tracing::TraceRecordingState traceRecording) const {
bool emitted = false;
sessions_.forEach([&](HostTargetSession& session) {
if (emitted) {
/**
* TraceRecordingState object is not copiable for performance reasons,
* because it could contain large Runtime sampling profile object.
*
* This approach would not work with multi-client debugger setup.
*/
return;
}
if (session.hasFuseboxClient()) {
session.emitTraceRecording(std::move(traceRecording));
emitted = true;
}
});
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,383 @@
/*
* 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 "HostCommand.h"
#include "InspectorInterfaces.h"
#include "InstanceTarget.h"
#include "NetworkIOAgent.h"
#include "PerfMonitorV2.h"
#include "ScopedExecutor.h"
#include "WeakList.h"
#include <optional>
#include <string>
#include <jsinspector-modern/tracing/TracingMode.h>
#include <jsinspector-modern/tracing/TracingState.h>
#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 HostTargetSession;
class HostAgent;
class HostTracingAgent;
class HostCommandSender;
class HostRuntimeBinding;
class HostTarget;
class HostTargetTraceRecording;
struct HostTargetMetadata {
std::optional<std::string> appDisplayName{};
std::optional<std::string> appIdentifier{};
std::optional<std::string> deviceName{};
std::optional<std::string> integrationName;
std::optional<std::string> platform{};
std::optional<std::string> reactNativeVersion{};
};
/**
* Receives events from a HostTarget. This is a shared interface that each
* React Native platform needs to implement in order to integrate with the
* debugging stack.
*/
class HostTargetDelegate : public LoadNetworkResourceDelegate {
public:
HostTargetDelegate() = default;
HostTargetDelegate(const HostTargetDelegate &) = delete;
HostTargetDelegate(HostTargetDelegate &&) = delete;
HostTargetDelegate &operator=(const HostTargetDelegate &) = delete;
HostTargetDelegate &operator=(HostTargetDelegate &&) = delete;
// 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;
}
};
struct OverlaySetPausedInDebuggerMessageRequest {
/**
* The message to display in the overlay. If nullopt, hide the overlay.
*/
std::optional<std::string> message;
/**
* Equality operator, useful for unit tests
*/
inline bool operator==(const OverlaySetPausedInDebuggerMessageRequest &rhs) const
{
return message == rhs.message;
}
};
virtual ~HostTargetDelegate() override;
/**
* Returns a metadata object describing the host. This is called on an
* initial response to @cdp ReactNativeApplication.enable.
*/
virtual HostTargetMetadata getMetadata() = 0;
/**
* 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;
/**
* Called when the debugger requests that the "paused in debugger" overlay be
* shown or hidden. If the message is nullopt, hide the overlay, otherwise
* show it with the given message. This is called on the inspector thread.
*
* If this method is called with a non-null message, it's guaranteed to
* eventually be called again with a null message. In all other respects,
* the timing and payload of these messages are fully controlled by the
* client.
*/
virtual void onSetPausedInDebuggerMessage(const OverlaySetPausedInDebuggerMessageRequest &request) = 0;
/**
* [Experimental] Called when the runtime has new data for the V2 Perf
* Monitor overlay. This is called on the inspector thread.
*/
virtual void unstable_onPerfIssueAdded(const PerfIssuePayload & /*issue*/) {}
/**
* Called by NetworkIOAgent on handling a `Network.loadNetworkResource` CDP
* request. Platform implementations should override this to perform a
* network request of the given URL, and use listener's callbacks on receipt
* of headers, data chunks, and errors.
*/
void loadNetworkResource(
const LoadNetworkResourceRequest & /*params*/,
ScopedExecutor<NetworkRequestListener> /*executor*/) override
{
throw NotImplementedException(
"LoadNetworkResourceDelegate.loadNetworkResource is not implemented by this host target delegate.");
}
/**
* [Experimental] Will be called at the CDP session initialization to get the
* trace recording that may have been stashed by the Host from the previous
* background session.
*
* \return the trace recording state if there is one that needs to be
* displayed, otherwise std::nullopt.
*/
virtual std::optional<tracing::TraceRecordingState> unstable_getTraceRecordingThatWillBeEmittedOnInitialization()
{
return std::nullopt;
}
};
/**
* The limited interface that HostTarget exposes to its associated
* sessions/agents.
*/
class HostTargetController final {
public:
explicit HostTargetController(HostTarget &target);
HostTargetDelegate &getDelegate();
bool hasInstance() const;
/**
* [Experimental] Install a runtime binding subscribing to new Performance
* Issues, which we broadcast to the V2 Perf Monitor overlay via
* \ref HostTargetDelegate::unstable_onPerfIssueAdded.
*/
void installPerfIssuesBinding();
/**
* Increments the target's pause overlay counter. The counter represents the
* exact number of Agents that have (concurrently) requested the pause
* overlay to be shown. It's the caller's responsibility to only call this
* when the pause overlay's requested state transitions from hidden to
* visible.
*/
void incrementPauseOverlayCounter();
/**
* Decrements the target's pause overlay counter. The counter represents the
* exact number of Agents that have (concurrently) requested the pause
* overlay to be shown. It's the caller's responsibility to only call this
* when the pause overlay's requested state transitions from hidden to
* visible.
* \returns false if the counter has reached 0, otherwise true.
*/
bool decrementPauseOverlayCounter();
/**
* Starts trace recording for this HostTarget.
*
* \param mode In which mode to start the trace recording.
* \return false if already tracing, true otherwise.
*/
bool startTracing(tracing::Mode mode);
/**
* Stops previously started trace recording.
*/
tracing::TraceRecordingState stopTracing();
private:
HostTarget &target_;
size_t pauseOverlayCounter_{0};
};
/**
* 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 HostTarget : public EnableExecutorFromThis<HostTarget> {
public:
/**
* Constructs a new HostTarget.
* \param delegate The HostTargetDelegate that will
* receive events from this HostTarget. The caller is responsible for ensuring
* that the HostTargetDelegate outlives this object.
* \param executor An executor that may be used to call methods on this
* HostTarget while it exists. \c create additionally guarantees that the
* executor will not be called after the HostTarget is destroyed.
* \note Copies of the provided executor may be destroyed on arbitrary
* threads, including after the HostTarget is destroyed. Callers must ensure
* that such destructor calls are safe - e.g. if using a lambda as the
* executor, all captured values must be safe to destroy from any thread.
*/
static std::shared_ptr<HostTarget> create(HostTargetDelegate &delegate, VoidExecutor executor);
HostTarget(const HostTarget &) = delete;
HostTarget(HostTarget &&) = delete;
HostTarget &operator=(const HostTarget &) = delete;
HostTarget &operator=(HostTarget &&) = delete;
~HostTarget();
/**
* Creates a new Session connected to this HostTarget, wrapped in an
* interface which is compatible with \c IInspector::addPage.
* The caller is responsible for destroying the connection before HostTarget
* is destroyed, on the same thread where HostTarget's constructor and
* destructor execute.
*/
std::unique_ptr<ILocalConnection> connect(std::unique_ptr<IRemoteConnection> connectionToFrontend);
/**
* Registers an instance with this HostTarget.
* \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 HostTarget is destroyed). \pre There isn't currently an instance
* registered with this HostTarget.
*/
InstanceTarget &registerInstance(InstanceTargetDelegate &delegate);
/**
* Unregisters an instance from this HostTarget.
* \param instance The InstanceTarget reference previously returned by
* registerInstance.
*/
void unregisterInstance(InstanceTarget &instance);
/**
* Sends an imperative command to the HostTarget. May be called from any
* thread.
*/
void sendCommand(HostCommand command);
/**
* Creates a new HostTracingAgent.
* This Agent is not owned by the HostTarget. The Agent will be destroyed at
* the end of the tracing session.
*
* \param state A reference to the state of the active trace recording.
*/
std::shared_ptr<HostTracingAgent> createTracingAgent(tracing::TraceRecordingState &state);
/**
* Starts trace recording for this HostTarget.
*
* \param mode In which mode to start the trace recording.
* \return false if already tracing, true otherwise.
*/
bool startTracing(tracing::Mode mode);
/**
* Stops previously started trace recording.
*/
tracing::TraceRecordingState stopTracing();
/**
* Returns the state of the background trace, running, stopped, or disabled
*/
tracing::TracingState tracingState() const;
/**
* Returns whether there is an active session with the Fusebox client, i.e.
* with Chrome DevTools Frontend fork for React Native.
*/
bool hasActiveSessionWithFuseboxClient() const;
/**
* Emits the trace recording for the first active session with the Fusebox
* client.
*
* @see \c hasActiveFrontendSession
*/
void emitTraceRecordingForFirstFuseboxClient(tracing::TraceRecordingState traceRecording) const;
/**
* Emits a system state changed event to all active sessions.
*/
void emitSystemStateChanged(bool isSingleHost) const;
private:
/**
* Constructs a new HostTarget.
* The caller must call setExecutor immediately afterwards.
* \param delegate The HostTargetDelegate that will
* receive events from this HostTarget. The caller is responsible for ensuring
* that the HostTargetDelegate outlives this object.
*/
HostTarget(HostTargetDelegate &delegate);
HostTargetDelegate &delegate_;
WeakList<HostTargetSession> sessions_;
HostTargetController 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 HostTarget, which it generally shouldn't).
std::shared_ptr<ExecutionContextManager> executionContextManager_;
std::shared_ptr<InstanceTarget> currentInstance_{nullptr};
std::unique_ptr<HostCommandSender> commandSender_;
std::unique_ptr<PerfMonitorUpdateHandler> perfMonitorUpdateHandler_;
std::unique_ptr<HostRuntimeBinding> perfMetricsBinding_;
/**
* Current pending trace recording, which encapsulates the configuration of
* the tracing session and the state.
*
* Should only be allocated when there is an active tracing session.
*/
std::unique_ptr<HostTargetTraceRecording> traceRecording_{nullptr};
inline HostTargetDelegate &getDelegate()
{
return delegate_;
}
inline bool hasInstance() const
{
return currentInstance_ != nullptr;
}
/**
* [Experimental] Install a runtime binding subscribing to new Peformance
* Issues, which we broadcast to the V2 Perf Monitor overlay via
* \ref HostTargetDelegate::unstable_onPerfMonitorUpdate.
*/
void installPerfIssuesBinding();
// Necessary to allow HostAgent to access HostTarget's internals in a
// controlled way (i.e. only HostTargetController gets friend access, while
// HostAgent itself doesn't).
friend class HostTargetController;
};
folly::dynamic createHostMetadataPayload(const HostTargetMetadata &metadata);
} // 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.
*/
#include "HostTargetTraceRecording.h"
#include "HostTarget.h"
namespace facebook::react::jsinspector_modern {
HostTargetTraceRecording::HostTargetTraceRecording(
tracing::Mode tracingMode,
HostTarget& hostTarget)
: tracingMode_(tracingMode), hostTarget_(hostTarget) {}
void HostTargetTraceRecording::setTracedInstance(
InstanceTarget* instanceTarget) {
// If HostTracingAgent is allocated, it means that there is an active tracing
// recording session.
if (hostTracingAgent_ != nullptr) {
hostTracingAgent_->setTracedInstance(instanceTarget);
}
}
void HostTargetTraceRecording::start() {
assert(
hostTracingAgent_ == nullptr &&
"Tracing Agent for the HostTarget was already initialized.");
state_ = tracing::TraceRecordingState{
.mode = tracingMode_,
.startTime = HighResTimeStamp::now(),
};
hostTracingAgent_ = hostTarget_.createTracingAgent(*state_);
}
tracing::TraceRecordingState HostTargetTraceRecording::stop() {
assert(
hostTracingAgent_ != nullptr &&
"TracingAgent for the HostTarget has not been initialized.");
hostTracingAgent_.reset();
assert(
state_.has_value() &&
"The state for this tracing session has not been initialized.");
auto state = std::move(*state_);
state_.reset();
return state;
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,86 @@
/*
* 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 "HostAgent.h"
#include "HostTarget.h"
#include "InstanceTarget.h"
#include <jsinspector-modern/tracing/TraceRecordingState.h>
#include <optional>
namespace facebook::react::jsinspector_modern {
/**
* A local representation of the Tracing "session".
*
* Owned by the HostTarget and should only be allocated during an active
* recording.
*
* Owns all allocated Tracing Agents. A single Target can have a single active
* Tracing Agent, but only as a std::weak_ptr.
*/
class HostTargetTraceRecording {
public:
explicit HostTargetTraceRecording(tracing::Mode tracingMode, HostTarget &hostTarget);
inline bool isBackgroundInitiated() const
{
return tracingMode_ == tracing::Mode::Background;
}
inline bool isUserInitiated() const
{
return tracingMode_ == tracing::Mode::CDP;
}
/**
* Updates the current traced Instance for this recording.
*/
void setTracedInstance(InstanceTarget *instanceTarget);
/**
* Starts the recording.
*
* Will allocate all Tracing Agents for all currently registered Targets.
*/
void start();
/**
* Stops the recording and drops the recording state.
*
* Will deallocate all Tracing Agents.
*/
tracing::TraceRecordingState stop();
private:
/**
* The mode in which this trace recording was initialized.
*/
tracing::Mode tracingMode_;
/**
* The Host for which this Trace Recording is going to happen.
*/
HostTarget &hostTarget_;
/**
* The state of the current Trace Recording.
* Only allocated if the recording is enabled.
*/
std::optional<tracing::TraceRecordingState> state_;
/**
* The TracingAgent of the targeted Host.
* Only allocated if the recording is enabled.
*/
std::shared_ptr<HostTracingAgent> hostTracingAgent_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,71 @@
/*
* 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/tracing/TracingState.h>
#include "HostTarget.h"
#include "HostTargetTraceRecording.h"
namespace facebook::react::jsinspector_modern {
bool HostTargetController::startTracing(tracing::Mode tracingMode) {
return target_.startTracing(tracingMode);
}
tracing::TraceRecordingState HostTargetController::stopTracing() {
return target_.stopTracing();
}
std::shared_ptr<HostTracingAgent> HostTarget::createTracingAgent(
tracing::TraceRecordingState& state) {
auto agent = std::make_shared<HostTracingAgent>(state);
agent->setTracedInstance(currentInstance_.get());
return agent;
}
bool HostTarget::startTracing(tracing::Mode tracingMode) {
if (traceRecording_ != nullptr) {
if (traceRecording_->isBackgroundInitiated() &&
tracingMode == tracing::Mode::CDP) {
traceRecording_.reset();
} else {
return false;
}
}
traceRecording_ =
std::make_unique<HostTargetTraceRecording>(tracingMode, *this);
traceRecording_->setTracedInstance(currentInstance_.get());
traceRecording_->start();
return true;
}
tracing::TraceRecordingState HostTarget::stopTracing() {
assert(traceRecording_ != nullptr && "No tracing in progress");
auto state = traceRecording_->stop();
traceRecording_.reset();
return state;
}
tracing::TracingState HostTarget::tracingState() const {
if (traceRecording_ == nullptr) {
return tracing::TracingState::Disabled;
}
if (traceRecording_->isBackgroundInitiated()) {
return tracing::TracingState::EnabledInBackgroundMode;
}
// This means we have a traceRecording_, but not running in the background.
// CDP initiated this trace so we should report as disabled.
return tracing::TracingState::EnabledInCDPMode;
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,90 @@
/*
* 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;
}
bool InspectorFlags::getAssertSingleHostState() const {
return loadFlagsAndAssertUnchanged().assertSingleHostState;
}
bool InspectorFlags::getFuseboxEnabled() const {
if (fuseboxDisabledForTest_) {
return false;
}
return loadFlagsAndAssertUnchanged().fuseboxEnabled;
}
bool InspectorFlags::getIsProfilingBuild() const {
return loadFlagsAndAssertUnchanged().isProfilingBuild;
}
bool InspectorFlags::getNetworkInspectionEnabled() const {
return loadFlagsAndAssertUnchanged().networkInspectionEnabled;
}
bool InspectorFlags::getPerfIssuesEnabled() const {
return loadFlagsAndAssertUnchanged().perfIssuesEnabled;
}
void InspectorFlags::dangerouslyResetFlags() {
*this = InspectorFlags{};
}
void InspectorFlags::dangerouslyDisableFuseboxForTest() {
fuseboxDisabledForTest_ = true;
}
const InspectorFlags::Values& InspectorFlags::loadFlagsAndAssertUnchanged()
const {
InspectorFlags::Values newValues = {
.assertSingleHostState =
ReactNativeFeatureFlags::fuseboxAssertSingleHostState(),
.fuseboxEnabled =
#if defined(REACT_NATIVE_DEBUGGER_ENABLED)
true,
#else
ReactNativeFeatureFlags::fuseboxEnabledRelease(),
#endif
.isProfilingBuild =
#if defined(REACT_NATIVE_DEBUGGER_MODE_PROD)
true,
#else
false,
#endif
.networkInspectionEnabled =
ReactNativeFeatureFlags::enableBridgelessArchitecture() &&
ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled(),
.perfIssuesEnabled = ReactNativeFeatureFlags::perfIssuesEnabled(),
};
if (cachedValues_.has_value() && !inconsistentFlagsStateLogged_) {
if (cachedValues_ != newValues) {
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;
}
}
cachedValues_ = newValues;
return cachedValues_.value();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,83 @@
/*
* 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 inspector backend should strictly assert that only
* a single host is registered.
*/
bool getAssertSingleHostState() const;
/**
* Flag determining if the modern CDP backend should be enabled.
*/
bool getFuseboxEnabled() const;
/**
* Flag determining if this is a profiling build
* (react_native.enable_fusebox_release).
*/
bool getIsProfilingBuild() const;
/**
* Flag determining if network inspection is enabled.
*/
bool getNetworkInspectionEnabled() const;
/**
* Flag determining if the V2 in-app Performance Monitor is enabled.
*/
bool getPerfIssuesEnabled() const;
/**
* Forcibly disable the main `getFuseboxEnabled()` flag. This should ONLY be
* used by `ReactInstanceIntegrationTest`.
*/
void dangerouslyDisableFuseboxForTest();
/**
* Reset flags to their upstream values. The caller must ensure any resources
* that have read previous flag values have been cleaned up.
*/
void dangerouslyResetFlags();
private:
struct Values {
bool assertSingleHostState;
bool fuseboxEnabled;
bool isProfilingBuild;
bool networkInspectionEnabled;
bool perfIssuesEnabled;
bool operator==(const Values &) const = default;
};
InspectorFlags() = default;
InspectorFlags(const InspectorFlags &) = delete;
InspectorFlags &operator=(const InspectorFlags &) = default;
~InspectorFlags() = default;
mutable std::optional<Values> cachedValues_;
mutable bool inconsistentFlagsStateLogged_{false};
bool fuseboxDisabledForTest_{false};
const Values &loadFlagsAndAssertUnchanged() const;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,231 @@
/*
* 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 "InspectorFlags.h"
#include <cassert>
#include <list>
#include <mutex>
#include <utility>
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() = default;
ILocalConnection::~ILocalConnection() = default;
IRemoteConnection::~IRemoteConnection() = default;
IInspector::~IInspector() = default;
IPageStatusListener::~IPageStatusListener() = default;
folly::dynamic targetCapabilitiesToDynamic(
const InspectorTargetCapabilities& capabilities) {
return folly::dynamic::object(
"nativePageReloads", capabilities.nativePageReloads)(
"nativeSourceCodeFetching", capabilities.nativeSourceCodeFetching)(
"prefersFuseboxFrontend", capabilities.prefersFuseboxFrontend);
}
namespace {
class InspectorImpl : public IInspector {
public:
InspectorImpl() {
systemStateListener_ = std::make_shared<SystemStateListener>(systemState_);
auto& inspectorFlags = InspectorFlags::getInstance();
if (inspectorFlags.getAssertSingleHostState()) {
registerPageStatusListener(systemStateListener_);
}
}
int addPage(
const std::string& description,
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;
InspectorSystemState getSystemState() const override;
private:
class SystemStateListener : public IPageStatusListener {
public:
explicit SystemStateListener(InspectorSystemState& state) : state_(state) {}
void unstable_onHostTargetAdded() override {
state_.registeredHostsCount++;
}
private:
InspectorSystemState& state_;
};
class Page {
public:
Page(
int id,
std::string description,
std::string vm,
ConnectFunc connectFunc,
InspectorTargetCapabilities capabilities);
operator InspectorPageDescription() const;
ConnectFunc getConnectFunc() const;
private:
int id_;
std::string description_;
std::string vm_;
ConnectFunc connectFunc_;
InspectorTargetCapabilities capabilities_;
};
mutable std::mutex mutex_;
int nextPageId_{1};
std::map<int, Page> pages_;
std::list<std::weak_ptr<IPageStatusListener>> listeners_;
InspectorSystemState systemState_{0};
std::shared_ptr<SystemStateListener> systemStateListener_;
};
InspectorImpl::Page::Page(
int id,
std::string description,
std::string vm,
ConnectFunc connectFunc,
InspectorTargetCapabilities capabilities)
: id_(id),
description_(std::move(description)),
vm_(std::move(vm)),
connectFunc_(std::move(connectFunc)),
capabilities_(capabilities) {}
InspectorImpl::Page::operator InspectorPageDescription() const {
return InspectorPageDescription{
.id = id_,
.description = description_,
.vm = vm_,
.capabilities = capabilities_,
};
}
InspectorImpl::ConnectFunc InspectorImpl::Page::getConnectFunc() const {
return connectFunc_;
}
int InspectorImpl::addPage(
const std::string& description,
const std::string& vm,
ConnectFunc connectFunc,
InspectorTargetCapabilities capabilities) {
std::scoped_lock lock(mutex_);
// Note: getPages guarantees insertion/addition order. As an implementation
// detail, incrementing page IDs takes advantage of std::map's key ordering.
int pageId = nextPageId_++;
assert(pages_.count(pageId) == 0 && "Unexpected duplicate page ID");
pages_.emplace(
pageId,
Page{pageId, description, vm, std::move(connectFunc), capabilities});
// Strong assumption: If prefersFuseboxFrontend is set, the page added is a
// HostTarget and not a legacy Hermes runtime target.
if (capabilities.prefersFuseboxFrontend) {
for (const auto& listenerWeak : listeners_) {
if (auto listener = listenerWeak.lock()) {
listener->unstable_onHostTargetAdded();
}
}
}
return pageId;
}
void InspectorImpl::removePage(int pageId) {
std::scoped_lock lock(mutex_);
if (pages_.erase(pageId) != 0) {
for (const 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;
// pages_ is a std::map keyed on an incremental id, so this is insertion
// ordered.
inspectorPages.reserve(pages_.size());
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);
}
InspectorSystemState InspectorImpl::getSystemState() const {
std::scoped_lock lock(mutex_);
return systemState_;
}
} // 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,171 @@
/*
* 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 {
bool nativePageReloads = false;
bool nativeSourceCodeFetching = false;
bool prefersFuseboxFrontend = false;
};
folly::dynamic targetCapabilitiesToDynamic(const InspectorTargetCapabilities &capabilities);
struct InspectorPageDescription {
const int id;
const std::string description;
const std::string vm;
const InspectorTargetCapabilities capabilities;
};
// Alias for backwards compatibility.
using InspectorPage = InspectorPageDescription;
struct InspectorSystemState {
/** The total count of pages registered during the app lifetime. */
int registeredHostsCount;
};
/// 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 unstable_onHostTargetAdded() {}
virtual void onPageRemoved(int /*pageId*/) {}
};
/// 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 &description,
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;
/**
* Called by the client to retrieve all debuggable pages.
* \returns A vector of page descriptions in the order in which they were
* added with \c addPage.
*/
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;
/**
* Get the current \c InspectorSystemState object.
*/
virtual InspectorSystemState getSystemState() const = 0;
};
class NotImplementedException : public std::exception {
public:
explicit NotImplementedException(std::string message) : msg_(std::move(message)) {}
const char *what() const noexcept override
{
return msg_.c_str();
}
private:
std::string msg_;
};
/// 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,396 @@
/*
* 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>
#include <utility>
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 deviceName,
std::string appName,
std::unique_ptr<InspectorPackagerConnectionDelegate> delegate) {
// No make_shared because the constructor is private
std::shared_ptr<InspectorPackagerConnection::Impl> impl(
new InspectorPackagerConnection::Impl(
std::move(url),
std::move(deviceName),
std::move(appName),
std::move(delegate)));
getInspectorInstance().registerPageStatusListener(impl);
return impl;
}
InspectorPackagerConnection::Impl::Impl(
std::string url,
std::string deviceName,
std::string appName,
std::unique_ptr<InspectorPackagerConnectionDelegate> delegate)
: url_(std::move(url)),
deviceName_(std::move(deviceName)),
appName_(std::move(appName)),
delegate_(std::move(delegate)) {}
void InspectorPackagerConnection::Impl::handleProxyMessage(
const 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(
const 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(
const 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 = 0;
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(
const 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(
const 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(
const 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"] = appName_ + " (" + deviceName_ + ")";
pageDescription["description"] = page.description + " [C++ connection]";
pageDescription["app"] = appName_;
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_) {
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(parsedJSON);
}
void InspectorPackagerConnection::Impl::didOpen() {
connected_ = true;
}
void InspectorPackagerConnection::Impl::didClose() {
connected_ = false;
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 && connected_;
}
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;
}
if (isConnected()) {
return;
}
reconnectPending_ = true;
delegate_->scheduleCallback(
[weakSelf = weak_from_this()] {
auto strongSelf = weakSelf.lock();
if (strongSelf && !strongSelf->closed_) {
strongSelf->reconnectPending_ = false;
if (strongSelf->isConnected()) {
return;
}
strongSelf->connect();
}
},
RECONNECT_DELAY);
}
void InspectorPackagerConnection::Impl::closeQuietly() {
closed_ = true;
disposeWebSocket();
}
void InspectorPackagerConnection::Impl::sendToPackager(
const folly::dynamic& message) {
if (!webSocket_) {
return;
}
webSocket_->send(folly::toJson(message));
}
void InspectorPackagerConnection::Impl::scheduleSendToPackager(
folly::dynamic message,
SessionId sourceSessionId,
const 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_(std::move(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 deviceName,
std::string appName,
std::unique_ptr<InspectorPackagerConnectionDelegate> delegate)
: impl_(
Impl::create(
std::move(url),
std::move(deviceName),
std::move(appName),
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,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.
*/
#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 deviceName The host device name.
* \param appName The application name.
* \param delegate An interface to platform-specific methods for creating a
* WebSocket, scheduling async work, etc.
*/
InspectorPackagerConnection(
std::string url,
std::string deviceName,
std::string appName,
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 and should schedule the callback on
* the inspector queue. 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,122 @@
/*
* 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 deviceName,
std::string appName,
std::unique_ptr<InspectorPackagerConnectionDelegate> delegate);
// InspectorPackagerConnection's public API
bool isConnected() const;
void connect();
void closeQuietly();
void sendEventToAllConnections(const std::string &event);
std::unique_ptr<ILocalConnection> removeConnectionForPage(const 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, const std::string &sourcePageId);
private:
struct Session {
std::unique_ptr<ILocalConnection> localConnection;
SessionId sessionId;
};
class RemoteConnection;
Impl(
std::string url,
std::string deviceName,
std::string appName,
std::unique_ptr<InspectorPackagerConnectionDelegate> delegate);
Impl(const Impl &) = delete;
Impl &operator=(const Impl &) = delete;
void handleDisconnect(const folly::const_dynamic_view &payload);
void handleConnect(const folly::const_dynamic_view &payload);
void handleWrappedEvent(const folly::const_dynamic_view &payload);
void handleProxyMessage(const folly::const_dynamic_view &message);
folly::dynamic pages();
void reconnect();
void closeAllConnections();
void disposeWebSocket();
void sendToPackager(const 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 didOpen() override;
virtual void didClose() override;
// IPageStatusListener methods
virtual void onPageRemoved(int pageId) override;
const std::string url_;
const std::string deviceName_;
const std::string appName_;
const std::unique_ptr<InspectorPackagerConnectionDelegate> delegate_;
std::unordered_map<std::string, Session> inspectorSessions_;
std::unique_ptr<IWebSocket> webSocket_;
bool connected_{false};
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,47 @@
/*
* 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;
}
CallbackRemoteConnection::CallbackRemoteConnection(
std::function<void(std::string)> handler)
: handler_(std::move(handler)) {}
void CallbackRemoteConnection::onMessage(std::string message) {
handler_(std::move(message));
}
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,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 "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 a callback function in IRemoteConnection.
*/
class CallbackRemoteConnection : public IRemoteConnection {
public:
/**
* Creates a new Connection that uses the given callback to receive messages
* from the backend.
*/
explicit CallbackRemoteConnection(std::function<void(std::string)> handler);
void onMessage(std::string message) override;
void onDisconnect() 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_;
};
/**
* An \c IRemoteConnection that does nothing.
*/
class NullRemoteConnection : public IRemoteConnection {
inline void onMessage(std::string /*message*/) override {}
inline void onDisconnect() override {}
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,200 @@
/*
* 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 "RuntimeTarget.h"
#include <jsinspector-modern/cdp/CdpJson.h>
#include <jsinspector-modern/tracing/PerformanceTracer.h>
#include <utility>
namespace facebook::react::jsinspector_modern {
namespace {
// The size of the timeline for the trace recording that happened in the
// background.
constexpr HighResDuration kBackgroundTracePerformanceTracerWindowSize =
HighResDuration::fromMilliseconds(20000);
} // namespace
InstanceAgent::InstanceAgent(
FrontendChannel frontendChannel,
InstanceTarget& target,
SessionState& sessionState)
: frontendChannel_(std::move(frontendChannel)),
target_(target),
sessionState_(sessionState) {
(void)target_;
}
bool InstanceAgent::handleRequest(const cdp::PreparsedRequest& req) {
if (req.method == "Runtime.enable") {
maybeSendExecutionContextCreatedNotification();
maybeSendPendingConsoleMessages();
// Fall through
}
if (runtimeAgent_ && runtimeAgent_->handleRequest(req)) {
return true;
}
return false;
}
void InstanceAgent::setCurrentRuntime(RuntimeTarget* runtimeTarget) {
auto previousRuntimeAgent = std::move(runtimeAgent_);
if (runtimeTarget != nullptr) {
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;
}
frontendChannel_(
cdp::jsonNotification("Runtime.executionContextDestroyed", params));
}
maybeSendExecutionContextCreatedNotification();
maybeSendPendingConsoleMessages();
}
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;
}
frontendChannel_(
cdp::jsonNotification("Runtime.executionContextCreated", params));
}
}
void InstanceAgent::sendConsoleMessage(SimpleConsoleMessage message) {
if (runtimeAgent_ && sessionState_.isRuntimeDomainEnabled) {
sendConsoleMessageImmediately(std::move(message));
} else {
sessionState_.pendingSimpleConsoleMessages.emplace_back(std::move(message));
}
}
static std::string consoleMessageTypeName(ConsoleAPIType type) {
switch (type) {
case ConsoleAPIType::kLog:
return "log";
case ConsoleAPIType::kDebug:
return "debug";
case ConsoleAPIType::kInfo:
return "info";
case ConsoleAPIType::kError:
return "error";
case ConsoleAPIType::kWarning:
return "warning";
case ConsoleAPIType::kDir:
return "dir";
case ConsoleAPIType::kDirXML:
return "dirxml";
case ConsoleAPIType::kTable:
return "table";
case ConsoleAPIType::kTrace:
return "trace";
case ConsoleAPIType::kStartGroup:
return "startGroup";
case ConsoleAPIType::kStartGroupCollapsed:
return "startGroupCollapsed";
case ConsoleAPIType::kEndGroup:
return "endGroup";
case ConsoleAPIType::kClear:
return "clear";
case ConsoleAPIType::kAssert:
return "assert";
case ConsoleAPIType::kTimeEnd:
return "timeEnd";
case ConsoleAPIType::kCount:
return "count";
default:
assert(false && "unknown console API type");
return "error";
}
}
void InstanceAgent::sendConsoleMessageImmediately(
SimpleConsoleMessage message) {
assert(runtimeAgent_ != nullptr);
folly::dynamic argsParam = folly::dynamic::array();
for (auto& arg : message.args) {
argsParam.push_back(folly::dynamic::object("type", "string")("value", arg));
}
frontendChannel_(
cdp::jsonNotification(
"Runtime.consoleAPICalled",
folly::dynamic::object("type", consoleMessageTypeName(message.type))(
"timestamp", message.timestamp)("args", std::move(argsParam))(
"executionContextId",
runtimeAgent_->getExecutionContextDescription().id)(
// We use the @cdp Runtime.consoleAPICalled `context` parameter to
// mark synthetic messages generated by the backend, i.e. not
// originating in a real `console.*` API call.
"context",
runtimeAgent_->getExecutionContextDescription().name +
"#InstanceAgent")));
}
void InstanceAgent::maybeSendPendingConsoleMessages() {
if (runtimeAgent_ != nullptr) {
auto messages = std::move(sessionState_.pendingSimpleConsoleMessages);
sessionState_.pendingSimpleConsoleMessages.clear();
for (auto& message : messages) {
sendConsoleMessageImmediately(std::move(message));
}
}
}
#pragma mark - Tracing
InstanceTracingAgent::InstanceTracingAgent(tracing::TraceRecordingState& state)
: tracing::TargetTracingAgent(state) {
auto& performanceTracer = tracing::PerformanceTracer::getInstance();
if (state.mode == tracing::Mode::Background) {
performanceTracer.startTracing(kBackgroundTracePerformanceTracerWindowSize);
} else {
performanceTracer.startTracing();
}
}
InstanceTracingAgent::~InstanceTracingAgent() {
auto& performanceTracer = tracing::PerformanceTracer::getInstance();
auto performanceTraceEvents = performanceTracer.stopTracing();
if (performanceTraceEvents) {
state_.instanceTracingProfiles.emplace_back(
tracing::InstanceTracingProfile{
.performanceTraceEvents = std::move(*performanceTraceEvents),
});
}
}
void InstanceTracingAgent::setTracedRuntime(RuntimeTarget* runtimeTarget) {
if (runtimeTarget != nullptr) {
runtimeTracingAgent_ = runtimeTarget->createTracingAgent(state_);
} else {
runtimeTracingAgent_ = nullptr;
}
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,94 @@
/*
* 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/InstanceTarget.h>
#include <jsinspector-modern/RuntimeAgent.h>
#include <jsinspector-modern/cdp/CdpJson.h>
#include <jsinspector-modern/tracing/InstanceTracingProfile.h>
#include <jsinspector-modern/tracing/TargetTracingAgent.h>
#include <functional>
namespace facebook::react::jsinspector_modern {
/**
* 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 hostAgent_ 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);
/**
* Send a console message to the frontend, or buffer it to be sent later.
*/
void sendConsoleMessage(SimpleConsoleMessage message);
private:
void maybeSendExecutionContextCreatedNotification();
void sendConsoleMessageImmediately(SimpleConsoleMessage message);
void maybeSendPendingConsoleMessages();
FrontendChannel frontendChannel_;
InstanceTarget &target_;
std::shared_ptr<RuntimeAgent> runtimeAgent_;
SessionState &sessionState_;
};
#pragma mark - Tracing
/**
* An Agent that handles Tracing events for a particular InstanceTarget.
*
* Lifetime of this agent is bound to the lifetime of the Tracing session -
* HostTargetTraceRecording and to the lifetime of the InstanceTarget.
*/
class InstanceTracingAgent : tracing::TargetTracingAgent {
public:
explicit InstanceTracingAgent(tracing::TraceRecordingState &state);
~InstanceTracingAgent();
/**
* Registers the RuntimeTarget with this tracing agent.
*/
void setTracedRuntime(RuntimeTarget *runtimeTarget);
private:
std::shared_ptr<RuntimeTracingAgent> runtimeTracingAgent_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,111 @@
/*
* 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>
#include <utility>
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(std::move(executor));
return instanceTarget;
}
InstanceTarget::InstanceTarget(
std::shared_ptr<ExecutionContextManager> executionContextManager,
InstanceTargetDelegate& delegate)
: delegate_(delegate),
executionContextManager_(std::move(executionContextManager)) {
(void)delegate_;
}
InstanceTargetDelegate::~InstanceTargetDelegate() = default;
std::shared_ptr<InstanceAgent> InstanceTarget::createAgent(
const FrontendChannel& channel,
SessionState& sessionState) {
auto instanceAgent =
std::make_shared<InstanceAgent>(channel, *this, sessionState);
instanceAgent->setCurrentRuntime(currentRuntime_.get());
agents_.insert(instanceAgent);
return instanceAgent;
}
std::shared_ptr<InstanceTracingAgent> InstanceTarget::createTracingAgent(
tracing::TraceRecordingState& state) {
auto agent = std::make_shared<InstanceTracingAgent>(state);
agent->setTracedRuntime(currentRuntime_.get());
tracingAgent_ = agent;
return agent;
}
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 HostTarget::unregisterInstance()?");
// Tracing Agents are owned by the HostTargetTraceRecording.
assert(
tracingAgent_.expired() &&
"InstanceTracingAgent must be destroyed before their InstanceTarget. Did you call HostTarget::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,
std::move(jsExecutor),
makeVoidExecutor(executorFromThis()));
agents_.forEach([currentRuntime = &*currentRuntime_](InstanceAgent& agent) {
agent.setCurrentRuntime(currentRuntime);
});
if (auto tracingAgent = tracingAgent_.lock()) {
// Registers the Runtime for tracing, if a Trace is currently being
// recorded.
tracingAgent->setTracedRuntime(currentRuntime_.get());
}
return *currentRuntime_;
}
void InstanceTarget::unregisterRuntime(RuntimeTarget& Runtime) {
assert(
currentRuntime_ && currentRuntime_.get() == &Runtime &&
"Invalid unregistration");
agents_.forEach(
[](InstanceAgent& agent) { agent.setCurrentRuntime(nullptr); });
if (auto tracingAgent = tracingAgent_.lock()) {
// Unregisters the Runtime for tracing, if a Trace is currently being
// recorded.
tracingAgent->setTracedRuntime(nullptr);
}
currentRuntime_.reset();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,123 @@
/*
* 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 <jsinspector-modern/tracing/TraceRecordingState.h>
#include <memory>
namespace facebook::react::jsinspector_modern {
class InstanceAgent;
class InstanceTracingAgent;
class HostTargetTraceRecording;
/**
* 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(const FrontendChannel &channel, SessionState &sessionState);
/**
* Creates a new InstanceTracingAgent.
* This Agent is not owned by the InstanceTarget. The Agent will be destroyed
* either before the InstanceTarget is destroyed, as part of the
* InstanceTarget unregistration in HostTarget, or at the end of the tracing
* session.
*
* \param state A reference to the state of the active trace recording.
*/
std::shared_ptr<InstanceTracingAgent> createTracingAgent(tracing::TraceRecordingState &state);
/**
* Registers a JS runtime with this InstanceTarget. \returns a reference to
* the created RuntimeTarget, which is owned by the \c InstanceTarget. All the
* requirements of \c RuntimeTarget::create must be met.
*/
RuntimeTarget &registerRuntime(RuntimeTargetDelegate &delegate, RuntimeExecutor executor);
/**
* Unregisters a JS runtime from this InstanceTarget. This destroys the \c
* RuntimeTarget, and it is no longer valid to use. Note that the \c
* RuntimeTargetDelegate& initially provided to \c registerRuntime may
* continue to be used as long as JavaScript execution continues in the
* runtime.
*/
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_;
/**
* This TracingAgent is owned by the HostTracingAgent, both are bound to
* the lifetime of their corresponding targets and the lifetime of the tracing
* session - HostTargetTraceRecording.
*/
std::weak_ptr<InstanceTracingAgent> tracingAgent_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,549 @@
/*
* 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 "NetworkIOAgent.h"
#include "InspectorFlags.h"
#include "Base64.h"
#include "Utf8.h"
#include <jsinspector-modern/network/NetworkHandler.h>
#include <sstream>
#include <tuple>
#include <utility>
#include <variant>
namespace facebook::react::jsinspector_modern {
static constexpr long DEFAULT_BYTES_PER_READ =
1048576; // 1MB (Chrome v112 default)
static constexpr unsigned long MAX_BYTES_PER_READ = 10485760; // 10MB
// https://github.com/chromium/chromium/blob/128.0.6593.1/content/browser/devtools/devtools_io_context.cc#L71-L73
static constexpr std::array kTextMIMETypePrefixes{
"text/",
"application/x-javascript",
"application/json",
"application/xml",
"application/javascript" // Not in Chromium but emitted by Metro
};
namespace {
struct InitStreamResult {
uint32_t httpStatusCode;
Headers headers;
std::shared_ptr<Stream> stream;
};
using InitStreamError = const std::string;
using StreamInitCallback =
std::function<void(std::variant<InitStreamError, InitStreamResult>)>;
using IOReadCallback =
std::function<void(std::variant<IOReadError, IOReadResult>)>;
/**
* Private class owning state and implementing the listener for a particular
* request
*
* NetworkRequestListener overrides are thread safe, all other methods must be
* called from the same thread.
*/
class Stream : public NetworkRequestListener,
public EnableExecutorFromThis<Stream> {
public:
Stream(const Stream& other) = delete;
Stream& operator=(const Stream& other) = delete;
Stream(Stream&& other) = default;
Stream& operator=(Stream&& other) noexcept = default;
/**
* Factory method to create a Stream with a callback for the initial result
* of a network request.
* \param executor An executor on which all processing of callbacks from
* the platform will be performed, and on which the passed callback will be
* called.
* \param initCb Will be called once either on receipt of HTTP headers or
* any prior error, using the given executor.
*/
static std::shared_ptr<Stream> create(
VoidExecutor executor,
const StreamInitCallback& initCb) {
std::shared_ptr<Stream> stream{new Stream(initCb)};
stream->setExecutor(std::move(executor));
return stream;
}
/**
* Agent-facing API. Enqueue a read request for up to maxBytesToRead
* bytes, starting from the end of the previous read.
* \param maxBytesToRead The maximum number of bytes to read from the
* source stream.
* \param callback Will be called using the executor passed to create()
* with the result of the read, or an error string.
*/
void read(long maxBytesToRead, const IOReadCallback& callback) {
pendingReadRequests_.emplace_back(maxBytesToRead, callback);
processPending();
}
/**
* Agent-facing API. Call the platform-provided cancelFunction, if any,
* call the error callbacks of any in-flight read requests, and the initial
* error callback if it has not already fulfilled with success or error.
*/
void cancel() {
if (cancelFunction_) {
(*cancelFunction_)();
}
error_ = "Cancelled";
if (initCb_) {
auto cb = std::move(initCb_);
(*cb)(InitStreamError{"Cancelled"});
}
// Respond to any in-flight read requests with an error.
processPending();
}
/**
* Begin implementation of NetworkRequestListener, to be called by platform
* HostTargetDelegate. Any of these methods may be called from any thread.
*/
void onData(std::string_view data) override {
data_ << data;
bytesReceived_ += data.length();
processPending();
}
void onHeaders(uint32_t httpStatusCode, const Headers& headers) override {
// Find content-type through case-insensitive search of headers.
for (const auto& [name, value] : headers) {
std::string lowerName = name;
std::transform(
lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
if (lowerName == "content-type") {
isText_ = isTextMimeType(value);
break;
};
}
// If we've already seen an error, the initial callback as already been
// called with it.
if (initCb_) {
auto cb = std::move(initCb_);
(*cb)(InitStreamResult{
.httpStatusCode = httpStatusCode,
.headers = headers,
.stream = this->shared_from_this()});
}
}
void onError(const std::string& message) override {
// Only call the error callback once.
if (!error_) {
error_ = message;
if (initCb_) {
auto cb = std::move(initCb_);
(*cb)(InitStreamError{message});
}
}
processPending();
}
void onCompletion() override {
completed_ = true;
processPending();
}
void setCancelFunction(std::function<void()> cancelFunction) override {
cancelFunction_ = std::move(cancelFunction);
}
~Stream() override {
// Cancel any incoming request, if the platform has provided a cancel
// callback.
if (cancelFunction_) {
(*cancelFunction_)();
}
}
/* End NetworkRequestListener */
private:
/**
* Private constructor. The caller must call setExecutor immediately
* afterwards.
*/
explicit Stream(const StreamInitCallback& initCb)
: initCb_(std::make_unique<StreamInitCallback>(initCb)) {}
void processPending() {
// Go through each pending request in insertion order - execute the
// callback and remove it from pending if it can be satisfied.
for (auto it = pendingReadRequests_.begin();
it != pendingReadRequests_.end();) {
auto maxBytesToRead = std::get<0>(*it);
auto callback = std::get<1>(*it);
if (error_) {
callback(IOReadError{*error_});
} else if (
completed_ || (bytesReceived_ - data_.tellg() >= maxBytesToRead)) {
try {
callback(respond(maxBytesToRead));
} catch (const std::runtime_error& error) {
callback(IOReadError{error.what()});
}
} else {
// Not yet received enough data
++it;
continue;
}
it = pendingReadRequests_.erase(it);
}
}
IOReadResult respond(long maxBytesToRead) {
std::vector<char> buffer(maxBytesToRead);
data_.read(buffer.data(), maxBytesToRead);
auto bytesRead = data_.gcount();
std::string output;
buffer.resize(bytesRead);
if (isText_) {
auto originalSize = buffer.size();
// Maybe resize to drop the last 1-3 bytes so that buffer is valid.
truncateToValidUTF8(buffer);
if (buffer.size() < originalSize) {
// Rewind the stream so that the next read starts from the start of
// the code point we're removing from this chunk.
data_.seekg(buffer.size() - originalSize, std::ios_base::cur);
}
output = std::string(buffer.begin(), buffer.begin() + buffer.size());
} else {
// Encode the slice as a base64 string.
output = base64Encode(std::string_view(buffer.data(), buffer.size()));
}
return IOReadResult{
.data = output,
.eof = output.length() == 0 && completed_,
.base64Encoded = !isText_};
}
// https://github.com/chromium/chromium/blob/128.0.6593.1/content/browser/devtools/devtools_io_context.cc#L70-L80
static bool isTextMimeType(const std::string& mimeType) {
for (auto& kTextMIMETypePrefix : kTextMIMETypePrefixes) {
if (mimeType.starts_with(kTextMIMETypePrefix)) {
return true;
}
}
return false;
}
bool completed_{false};
bool isText_{false};
std::optional<std::string> error_;
std::stringstream data_;
long bytesReceived_{0};
std::optional<std::function<void()>> cancelFunction_{std::nullopt};
std::unique_ptr<StreamInitCallback> initCb_;
std::vector<std::tuple<long /* bytesToRead */, IOReadCallback>>
pendingReadRequests_;
};
} // namespace
bool NetworkIOAgent::handleRequest(
const cdp::PreparsedRequest& req,
LoadNetworkResourceDelegate& delegate) {
if (req.method == "Network.loadNetworkResource") {
handleLoadNetworkResource(req, delegate);
return true;
} else if (req.method == "IO.read") {
handleIoRead(req);
return true;
} else if (req.method == "IO.close") {
handleIoClose(req);
return true;
}
if (InspectorFlags::getInstance().getNetworkInspectionEnabled()) {
auto& networkHandler = NetworkHandler::getInstance();
// @cdp Network.enable support is experimental.
if (req.method == "Network.enable") {
networkHandler.setFrontendChannel(frontendChannel_);
networkHandler.enable();
// NOTE: Domain enable/disable responses are sent by HostAgent.
return false;
}
// @cdp Network.disable support is experimental.
if (req.method == "Network.disable") {
networkHandler.disable();
// NOTE: Domain enable/disable responses are sent by HostAgent.
return false;
}
// @cdp Network.getResponseBody support is experimental.
if (req.method == "Network.getResponseBody") {
handleGetResponseBody(req);
return true;
}
}
return false;
}
void NetworkIOAgent::handleLoadNetworkResource(
const cdp::PreparsedRequest& req,
LoadNetworkResourceDelegate& delegate) {
long long requestId = req.id;
LoadNetworkResourceRequest params;
if (!req.params.isObject()) {
frontendChannel_(
cdp::jsonError(
req.id,
cdp::ErrorCode::InvalidParams,
"Invalid params: not an object."));
return;
}
if ((req.params.count("url") == 0u) || !req.params.at("url").isString()) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InvalidParams,
"Invalid params: url is missing or not a string."));
return;
} else {
params.url = req.params.at("url").asString();
}
// This is an opaque identifier, but an incrementing integer in a string is
// consistent with Chrome.
StreamID streamId = std::to_string(nextStreamId_++);
auto stream = Stream::create(
executor_,
[streamId,
requestId,
frontendChannel = frontendChannel_,
streamsWeak = std::weak_ptr(streams_)](auto resultOrError) {
NetworkResource resource;
std::string cdpError;
if (auto* error = std::get_if<InitStreamError>(&resultOrError)) {
resource = NetworkResource{.success = false, .netErrorName = *error};
} else if (
auto* result = std::get_if<InitStreamResult>(&resultOrError)) {
if (result->httpStatusCode >= 200 && result->httpStatusCode < 300) {
resource = NetworkResource{
.success = true,
.stream = streamId,
.httpStatusCode = result->httpStatusCode,
.headers = result->headers};
} else if (result->httpStatusCode >= 400) {
resource = NetworkResource{
.success = false,
.httpStatusCode = result->httpStatusCode,
.headers = result->headers};
} else {
// We can't deal with <200 or 3xx reponses here (though they may be
// transparently handled by the delegate). Return a CDP error (not
// an unsuccesful resource) to the frontend so that it falls back to
// a direct fetch.
cdpError = "Handling of status " +
std::to_string(result->httpStatusCode) + " not implemented.";
}
} else {
assert(false && "Unhandled IO init result type");
}
if (cdpError.length() > 0 || !resource.success) {
// Release and destroy the stream after the calling executor returns.
// ~Stream will handle cancelling any download in progress.
if (auto streams = streamsWeak.lock()) {
streams->erase(streamId);
}
}
frontendChannel(
cdpError.length()
? cdp::jsonError(
requestId, cdp::ErrorCode::InternalError, cdpError)
: cdp::jsonResult(
requestId,
folly::dynamic::object(
"resource", resource.toDynamic())));
});
// Begin the network request on the platform, passing an executor scoped to
// a Stream (a NetworkRequestListener), which the implementation will call
// back into.
delegate.loadNetworkResource(params, stream->executorFromThis());
// Retain the stream only if delegate.loadNetworkResource does not throw.
streams_->emplace(streamId, stream);
}
void NetworkIOAgent::handleIoRead(const cdp::PreparsedRequest& req) {
long long requestId = req.id;
if (!req.params.isObject()) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InvalidParams,
"Invalid params: not an object."));
return;
}
if ((req.params.count("handle") == 0u) ||
!req.params.at("handle").isString()) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InvalidParams,
"Invalid params: handle is missing or not a string."));
return;
}
std::optional<int64_t> size = std::nullopt;
if ((req.params.count("size") != 0u) && req.params.at("size").isInt()) {
size = req.params.at("size").asInt();
if (size > MAX_BYTES_PER_READ) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InvalidParams,
"Invalid params: size cannot be greater than 10MB."));
return;
}
}
auto streamId = req.params.at("handle").asString();
auto it = streams_->find(streamId);
if (it == streams_->end()) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InternalError,
"Stream not found with handle " + streamId));
return;
} else {
it->second->read(
size ? *size : DEFAULT_BYTES_PER_READ,
[requestId,
frontendChannel = frontendChannel_,
streamId,
streamsWeak = std::weak_ptr(streams_)](auto resultOrError) {
if (auto* error = std::get_if<IOReadError>(&resultOrError)) {
// NB: Chrome DevTools calls IO.close after a read error, so any
// continuing download or retained data is cleaned up at that point.
frontendChannel(
cdp::jsonError(
requestId, cdp::ErrorCode::InternalError, *error));
} else if (auto* result = std::get_if<IOReadResult>(&resultOrError)) {
frontendChannel(cdp::jsonResult(requestId, result->toDynamic()));
} else {
assert(false && "Unhandled IO read result type");
}
});
return;
}
}
void NetworkIOAgent::handleIoClose(const cdp::PreparsedRequest& req) {
long long requestId = req.id;
if (!req.params.isObject()) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InvalidParams,
"Invalid params: not an object."));
return;
}
if ((req.params.count("handle") == 0u) ||
!req.params.at("handle").isString()) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InvalidParams,
"Invalid params: handle is missing or not a string."));
return;
}
auto streamId = req.params.at("handle").asString();
auto it = streams_->find(streamId);
if (it == streams_->end()) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InternalError,
"Stream not found: " + streamId));
} else {
it->second->cancel();
streams_->erase(it->first);
frontendChannel_(cdp::jsonResult(requestId));
}
}
void NetworkIOAgent::handleGetResponseBody(const cdp::PreparsedRequest& req) {
long long requestId = req.id;
if (!req.params.isObject()) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InvalidParams,
"Invalid params: not an object."));
return;
}
if ((req.params.count("requestId") == 0u) ||
!req.params.at("requestId").isString()) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InvalidParams,
"Invalid params: requestId is missing or not a string."));
return;
}
auto& networkHandler = NetworkHandler::getInstance();
if (!networkHandler.isEnabled()) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InvalidRequest,
"Invalid request: The \"Network\" domain is not enabled."));
return;
}
auto storedResponse =
networkHandler.getResponseBody(req.params.at("requestId").asString());
if (!storedResponse) {
frontendChannel_(
cdp::jsonError(
requestId,
cdp::ErrorCode::InternalError,
"Internal error: Could not retrieve response body for the given requestId."));
return;
}
std::string responseBody;
bool base64Encoded = false;
std::tie(responseBody, base64Encoded) = *storedResponse;
auto result = GetResponseBodyResult{
.body = responseBody,
.base64Encoded = base64Encoded,
};
frontendChannel_(cdp::jsonResult(requestId, result.toDynamic()));
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,276 @@
/*
* 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 "ScopedExecutor.h"
#include <folly/dynamic.h>
#include <jsinspector-modern/cdp/CdpJson.h>
#include <string>
#include <unordered_map>
#include <utility>
namespace facebook::react::jsinspector_modern {
using StreamID = const std::string;
using Headers = std::map<std::string, std::string>;
using IOReadError = const std::string;
namespace {
class Stream; // Defined in NetworkIOAgent.cpp
using StreamsMap = std::unordered_map<std::string, std::shared_ptr<Stream>>;
} // namespace
struct LoadNetworkResourceRequest {
std::string url;
};
struct ReadStreamParams {
StreamID handle;
std::optional<unsigned long> size;
std::optional<unsigned long> offset;
};
struct NetworkResource {
bool success{};
std::optional<std::string> stream{};
std::optional<uint32_t> httpStatusCode{};
std::optional<std::string> netErrorName{};
std::optional<Headers> headers{};
folly::dynamic toDynamic() const
{
auto dynamicResource = folly::dynamic::object("success", success);
if (success) { // stream IFF successful
assert(stream);
dynamicResource("stream", *stream);
}
if (netErrorName) { // Only if unsuccessful
assert(!success);
dynamicResource("netErrorName", *netErrorName);
}
if (httpStatusCode) { // Guaranteed if successful
dynamicResource("httpStatusCode", *httpStatusCode);
} else {
assert(!success);
}
if (headers) { // Guaranteed if successful
auto dynamicHeaders = folly::dynamic::object();
for (const auto &pair : *headers) {
dynamicHeaders(pair.first, pair.second);
}
dynamicResource("headers", std::move(dynamicHeaders));
} else {
assert(!success);
}
return dynamicResource;
}
};
struct IOReadResult {
std::string data;
bool eof;
bool base64Encoded;
folly::dynamic toDynamic() const
{
auto obj = folly::dynamic::object("data", data);
obj("eof", eof);
obj("base64Encoded", base64Encoded);
return obj;
}
};
struct GetResponseBodyResult {
std::string body;
bool base64Encoded;
folly::dynamic toDynamic() const
{
folly::dynamic params = folly::dynamic::object;
params["body"] = body;
params["base64Encoded"] = base64Encoded;
return params;
}
};
/**
* Passed to `loadNetworkResource`, provides callbacks for processing incoming
* data and other events.
*/
class NetworkRequestListener {
public:
NetworkRequestListener() = default;
NetworkRequestListener(const NetworkRequestListener &) = delete;
NetworkRequestListener &operator=(const NetworkRequestListener &) = delete;
NetworkRequestListener(NetworkRequestListener &&) noexcept = default;
NetworkRequestListener &operator=(NetworkRequestListener &&) noexcept = default;
virtual ~NetworkRequestListener() = default;
/**
* To be called by the delegate on receipt of response headers, including
* on "unsuccessful" status codes.
*
* \param httpStatusCode The HTTP status code received.
* \param headers Response headers as an unordered_map.
*/
virtual void onHeaders(uint32_t httpStatusCode, const Headers &headers) = 0;
/**
* To be called by the delegate on receipt of data chunks.
* \param data The data received.
*/
virtual void onData(std::string_view data) = 0;
/**
* To be called by the delegate on any error with the request, either before
* headers are received or for a subsequent interrupion.
*
* \param message A short, human-readable message, which may be forwarded to
* the CDP client either in the `loadNetworkResource` response (if headers
* were not yet received), or as a CDP error in response to a subsequent
* `IO.read`.
*/
virtual void onError(const std::string &message) = 0;
/**
* To be called by the delegate on successful completion of the request.
* Delegates must call *either* onCompletion() or onError() exactly once.
*/
virtual void onCompletion() = 0;
/**
* Optionally (preferably) used to give NetworkIOAgent
a way to cancel an
* in-progress download.
*
* \param cancelFunction A function that can be called to cancel a download,
* may be called before or after the download is complete.
*/
virtual void setCancelFunction(std::function<void()> cancelFunction) = 0;
};
/**
* Implemented by the HostTargetDelegate per-platform to perform network
* requests.
*/
class LoadNetworkResourceDelegate {
public:
LoadNetworkResourceDelegate() = default;
LoadNetworkResourceDelegate(const LoadNetworkResourceDelegate &) = delete;
LoadNetworkResourceDelegate &operator=(const LoadNetworkResourceDelegate &) = delete;
LoadNetworkResourceDelegate(LoadNetworkResourceDelegate &&) noexcept = delete;
LoadNetworkResourceDelegate &operator=(LoadNetworkResourceDelegate &&) noexcept = delete;
virtual ~LoadNetworkResourceDelegate() = default;
/**
* Called by NetworkIOAgent on handling a
* `Network.loadNetworkResource` CDP request. Platform implementations should
* override this to perform a network request of the given URL, and use
* listener's callbacks (via the executor) on receipt of headers, data chunks,
* and errors.
*
* \param params A LoadNetworkResourceRequest, including the url.
* \param executor A listener-scoped executor used by the delegate to execute
* listener callbacks on headers, data chunks, and errors. Implementations
* *should* call listener->setCancelFunction() to provide a lambda that can be
* called to abort any in-flight network operation that is no longer needed.
*/
virtual void loadNetworkResource(
[[maybe_unused]] const LoadNetworkResourceRequest &params,
[[maybe_unused]] ScopedExecutor<NetworkRequestListener> executor) = 0;
};
/**
* Provides an agent for handling CDP's Network.loadNetworkResource, IO.read and
* IO.close.
*
* Owns state of all in-progress and completed HTTP requests - ensure
* IO.close is used to free resources once consumed.
*
* Public methods must be called the same thread as the given executor.
*/
class NetworkIOAgent {
public:
/**
* \param frontendChannel A channel used to send responses to the
* frontend.
* \param executor An executor used for any callbacks provided, and for
* processing incoming data or other events from network operations.
*/
NetworkIOAgent(FrontendChannel frontendChannel, VoidExecutor executor)
: frontendChannel_(frontendChannel), executor_(executor), streams_(std::make_shared<StreamsMap>())
{
}
/**
* 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, LoadNetworkResourceDelegate &delegate);
private:
/**
* A channel used to send responses and events to the frontend.
*/
FrontendChannel frontendChannel_;
/**
* An executor used to create NetworkRequestListener-scoped executors for the
* delegate.
*/
VoidExecutor executor_;
/**
* Map of stream objects, which contain data received, accept read requests
* and listen for delegate events. Delegates have a scoped executor for Stream
* instances, but Streams will not live beyond the destruction of this
* NetworkIOAgent instance + executor scope.
*
* This is a shared_ptr so that we may capture a weak_ptr in our
* Stream::create callback without creating a cycle.
*/
std::shared_ptr<StreamsMap> streams_;
/**
* Stream IDs are strings of an incrementing integer, unique within each
* NewtworkIOAgent instance. This stores the next one to use.
*/
unsigned long nextStreamId_{0};
/**
* Begin loading an HTTP resource, delegating platform-specific
* implementation, responding to the frontend on headers received or on error.
* Does not catch exceptions thrown by the delegate (such as
* NotImplementedException).
*/
void handleLoadNetworkResource(const cdp::PreparsedRequest &req, LoadNetworkResourceDelegate &delegate);
/**
* Handle an IO.read CDP request. Emit a chunk of data from the stream, once
* enough has been downloaded, or report an error.
*/
void handleIoRead(const cdp::PreparsedRequest &req);
/**
* Handle an IO.close CDP request. Safely aborts any in-flight request.
* Reports CDP ok if the stream is found, or a CDP error if not.
*/
void handleIoClose(const cdp::PreparsedRequest &req);
/**
* Handle a Network.getResponseBody CDP request.
*/
void handleGetResponseBody(const cdp::PreparsedRequest &req);
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,29 @@
/*
* 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 "PerfMonitorV2.h"
#include "HostTarget.h"
#include <folly/json.h>
#include <react/timing/primitives.h>
namespace facebook::react::jsinspector_modern {
void PerfMonitorUpdateHandler::handlePerfIssueAdded(
const std::string& message) {
auto payload = folly::parseJson(message);
if (payload.isObject()) {
delegate_.unstable_onPerfIssueAdded(
PerfIssuePayload{
.name = payload["name"].asString(),
.severity = payload["severity"].asString(),
});
}
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,38 @@
/*
* 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 <string>
namespace facebook::react::jsinspector_modern {
class HostTargetDelegate;
struct PerfIssuePayload {
std::string name;
std::string severity;
};
/**
* [Experimental] Utility to handle performance metrics updates received from
* the runtime and forward update events to the V2 Perf Monitor UI.
*/
class PerfMonitorUpdateHandler {
public:
explicit PerfMonitorUpdateHandler(HostTargetDelegate &delegate) : delegate_(delegate) {}
/**
* Handle a new "__react_native_perf_issues_reporter" message.
*/
void handlePerfIssueAdded(const std::string &message);
private:
HostTargetDelegate &delegate_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,65 @@
# 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
header_search_paths = []
if ENV['USE_FRAMEWORKS']
header_search_paths << "\"$(PODS_TARGET_SRCROOT)/..\""
end
header_dir = 'jsinspector-modern'
module_name = "jsinspector_modern"
Pod::Spec.new do |s|
s.name = "React-jsinspector"
s.version = version
s.summary = "React Native subsystem for modern debugging over the Chrome DevTools Protocol (CDP)"
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 = podspec_sources("*.{cpp,h}", "*.h")
s.header_dir = header_dir
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"DEFINES_MODULE" => "YES"
}.merge!(ENV['USE_FRAMEWORKS'] ? {
"PUBLIC_HEADERS_FOLDER_PATH" => "#{module_name}.framework/Headers/#{header_dir}"
} : {})
resolve_use_frameworks(s, module_name: module_name)
add_dependency(s, "React-oscompat") # Needed for USE_FRAMEWORKS=dynamic
s.dependency "React-featureflags"
add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"])
s.dependency "React-jsi"
add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp')
add_dependency(s, "React-jsinspectornetwork", :framework_name => 'jsinspector_modernnetwork')
add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing')
s.dependency "React-perflogger", version
add_dependency(s, "React-oscompat")
add_dependency(s, "React-utils", :additional_framework_paths => ["react/utils/platform/ios"])
if use_hermes()
s.dependency "hermes-engine"
end
add_rn_third_party_dependencies(s)
add_rncore_dependency(s)
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/FallbackRuntimeTargetDelegate.h>
#include <jsinspector-modern/HostTarget.h>
#include <jsinspector-modern/InstanceTarget.h>
#include <jsinspector-modern/RuntimeTarget.h>
#include <jsinspector-modern/ScopedExecutor.h>
#include <jsinspector-modern/SessionState.h>

View File

@@ -0,0 +1,169 @@
/*
* 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"
#include <utility>
namespace facebook::react::jsinspector_modern {
RuntimeAgent::RuntimeAgent(
FrontendChannel frontendChannel,
RuntimeTargetController& targetController,
ExecutionContextDescription executionContextDescription,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate> delegate)
: frontendChannel_(std::move(frontendChannel)),
targetController_(targetController),
sessionState_(sessionState),
delegate_(std::move(delegate)),
executionContextDescription_(std::move(executionContextDescription)) {
for (auto& [name, contextSelectors] : sessionState_.subscribedBindings) {
if (matchesAny(executionContextDescription_, contextSelectors)) {
targetController_.installBindingHandler(name);
}
}
if (sessionState_.isRuntimeDomainEnabled) {
targetController_.notifyDomainStateChanged(
RuntimeTargetController::Domain::Runtime, true, *this);
}
if (sessionState_.isLogDomainEnabled) {
targetController_.notifyDomainStateChanged(
RuntimeTargetController::Domain::Log, true, *this);
}
if (sessionState_.isNetworkDomainEnabled) {
targetController_.notifyDomainStateChanged(
RuntimeTargetController::Domain::Network, true, *this);
}
}
bool RuntimeAgent::handleRequest(const cdp::PreparsedRequest& req) {
if (req.method == "Runtime.addBinding") {
std::string bindingName = req.params["name"].getString();
// Check if the binding has a context selector that matches the current
// context. The state will have been updated by HostAgent by the time we
// receive the request.
// NOTE: We DON'T do the reverse for removeBinding, for reasons explained
// in the implementation of HostAgent.
auto it = sessionState_.subscribedBindings.find(bindingName);
if (it != sessionState_.subscribedBindings.end()) {
auto contextSelectors = it->second;
if (matchesAny(executionContextDescription_, contextSelectors)) {
targetController_.installBindingHandler(bindingName);
}
}
// We are not responding to this request, just processing a side effect.
return false;
}
if (req.method == "Runtime.enable" || req.method == "Runtime.disable") {
targetController_.notifyDomainStateChanged(
RuntimeTargetController::Domain::Runtime,
sessionState_.isRuntimeDomainEnabled,
*this);
// Fall through
} else if (req.method == "Log.enable" || req.method == "Log.disable") {
targetController_.notifyDomainStateChanged(
RuntimeTargetController::Domain::Log,
sessionState_.isLogDomainEnabled,
*this);
// Fall through
} else if (
req.method == "Network.enable" || req.method == "Network.disable") {
targetController_.notifyDomainStateChanged(
RuntimeTargetController::Domain::Network,
sessionState_.isNetworkDomainEnabled,
*this);
// We are not responding to this request, just processing a side effect.
return false;
}
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) == 0u) {
return;
}
frontendChannel_(
cdp::jsonNotification(
"Runtime.bindingCalled",
folly::dynamic::object(
"executionContextId", executionContextDescription_.id)(
"name", bindingName)("payload", payload)));
}
RuntimeAgent::ExportedState RuntimeAgent::getExportedState() {
return {
.delegateState = delegate_ ? delegate_->getExportedState() : nullptr,
};
}
RuntimeAgent::~RuntimeAgent() {
if (sessionState_.isRuntimeDomainEnabled) {
targetController_.notifyDomainStateChanged(
RuntimeTargetController::Domain::Runtime, false, *this);
}
if (sessionState_.isLogDomainEnabled) {
targetController_.notifyDomainStateChanged(
RuntimeTargetController::Domain::Log, false, *this);
}
if (sessionState_.isNetworkDomainEnabled) {
targetController_.notifyDomainStateChanged(
RuntimeTargetController::Domain::Network, false, *this);
}
// 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();
}
#pragma mark - Tracing
RuntimeTracingAgent::RuntimeTracingAgent(
tracing::TraceRecordingState& state,
RuntimeTargetController& targetController)
: tracing::TargetTracingAgent(state), targetController_(targetController) {
if (state.mode == tracing::Mode::CDP) {
targetController_.enableSamplingProfiler();
}
}
RuntimeTracingAgent::~RuntimeTracingAgent() {
if (state_.mode == tracing::Mode::CDP) {
targetController_.disableSamplingProfiler();
auto profile = targetController_.collectSamplingProfile();
state_.runtimeSamplingProfiles.emplace_back(std::move(profile));
}
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,112 @@
/*
* 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/cdp/CdpJson.h>
#include <jsinspector-modern/tracing/RuntimeSamplingProfile.h>
#include <jsinspector-modern/tracing/TargetTracingAgent.h>
#include <jsinspector-modern/tracing/TraceRecordingState.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 Host).
* \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,
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_;
};
#pragma mark - Tracing
/**
* An Agent that handles Tracing events for a particular RuntimeTarget.
*
* Lifetime of this agent is bound to the lifetime of the Tracing session -
* HostTargetTraceRecording and to the lifetime of the RuntimeTarget.
*/
class RuntimeTracingAgent : tracing::TargetTracingAgent {
public:
explicit RuntimeTracingAgent(tracing::TraceRecordingState &state, RuntimeTargetController &targetController);
~RuntimeTracingAgent();
private:
RuntimeTargetController &targetController_;
};
} // 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 <jsinspector-modern/cdp/CdpJson.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,279 @@
/*
* 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>
#include <jsinspector-modern/tracing/PerformanceTracer.h>
#include <utility>
using namespace facebook::jsi;
namespace facebook::react::jsinspector_modern {
namespace {
void emitSessionStatusChangeForObserverWithValue(
jsi::Runtime& runtime,
const jsi::Value& value) {
auto globalObj = runtime.global();
auto observer =
globalObj.getPropertyAsObject(runtime, "__DEBUGGER_SESSION_OBSERVER__");
auto onSessionStatusChange =
observer.getPropertyAsFunction(runtime, "onSessionStatusChange");
onSessionStatusChange.call(runtime, value);
}
} // namespace
std::shared_ptr<RuntimeTarget> RuntimeTarget::create(
const ExecutionContextDescription& executionContextDescription,
RuntimeTargetDelegate& delegate,
RuntimeExecutor jsExecutor,
VoidExecutor selfExecutor) {
std::shared_ptr<RuntimeTarget> runtimeTarget{new RuntimeTarget(
executionContextDescription, delegate, std::move(jsExecutor))};
runtimeTarget->setExecutor(std::move(selfExecutor));
runtimeTarget->installGlobals();
return runtimeTarget;
}
RuntimeTarget::RuntimeTarget(
ExecutionContextDescription executionContextDescription,
RuntimeTargetDelegate& delegate,
RuntimeExecutor jsExecutor)
: executionContextDescription_(std::move(executionContextDescription)),
delegate_(delegate),
jsExecutor_(std::move(jsExecutor)) {}
void RuntimeTarget::installGlobals() {
// NOTE: RuntimeTarget::installConsoleHandler is in RuntimeTargetConsole.cpp
installConsoleHandler();
// NOTE: RuntimeTarget::installDebuggerSessionObserver is in
// RuntimeTargetDebuggerSessionObserver.cpp
installDebuggerSessionObserver();
// NOTE: RuntimeTarget::installNetworkReporterAPI is in
// RuntimeTargetNetwork.cpp
installNetworkReporterAPI();
}
std::shared_ptr<RuntimeAgent> RuntimeTarget::createAgent(
const 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_,
jsExecutor_));
agents_.insert(runtimeAgent);
return runtimeAgent;
}
std::shared_ptr<RuntimeTracingAgent> RuntimeTarget::createTracingAgent(
tracing::TraceRecordingState& state) {
auto agent = std::make_shared<RuntimeTracingAgent>(state, controller_);
tracingAgent_ = agent;
return agent;
}
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()?");
// Tracing Agents are owned by the HostTargetTraceRecording.
assert(
tracingAgent_.expired() &&
"RuntimeTracingAgent must be destroyed before their InstanceTarget. 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.
}
});
}
void RuntimeTarget::emitDebuggerSessionCreated() {
jsExecutor_([selfExecutor = executorFromThis()](jsi::Runtime& runtime) {
try {
emitSessionStatusChangeForObserverWithValue(runtime, jsi::Value(true));
} catch (jsi::JSError&) {
// Suppress any errors, they should not be visible to the user
// and should not affect runtime.
}
});
}
void RuntimeTarget::emitDebuggerSessionDestroyed() {
jsExecutor_([selfExecutor = executorFromThis()](jsi::Runtime& runtime) {
try {
emitSessionStatusChangeForObserverWithValue(runtime, jsi::Value(false));
} catch (jsi::JSError&) {
// Suppress any errors, they should not be visible to the user
// and should not affect runtime.
}
});
}
void RuntimeTarget::enableSamplingProfiler() {
delegate_.enableSamplingProfiler();
}
void RuntimeTarget::disableSamplingProfiler() {
delegate_.disableSamplingProfiler();
}
tracing::RuntimeSamplingProfile RuntimeTarget::collectSamplingProfile() {
return delegate_.collectSamplingProfile();
}
void RuntimeTarget::notifyDomainStateChanged(
Domain domain,
bool enabled,
const RuntimeAgent& notifyingAgent) {
auto [domainStateChangedLocally, domainStateChangedGlobally] =
processDomainChange(domain, enabled, notifyingAgent);
switch (domain) {
case Domain::Log:
case Domain::Runtime: {
auto otherDomain = domain == Domain::Log ? Domain::Runtime : Domain::Log;
// There should be an agent that enables both Log and Runtime domains.
if (!agentsByEnabledDomain_[otherDomain].contains(&notifyingAgent)) {
break;
}
if (domainStateChangedGlobally && enabled) {
assert(agentsWithRuntimeAndLogDomainsEnabled_ == 0);
emitDebuggerSessionCreated();
++agentsWithRuntimeAndLogDomainsEnabled_;
} else if (domainStateChangedGlobally) {
assert(agentsWithRuntimeAndLogDomainsEnabled_ == 1);
emitDebuggerSessionDestroyed();
--agentsWithRuntimeAndLogDomainsEnabled_;
} else if (domainStateChangedLocally && enabled) {
// This is a case when given domain was already enabled by other Agent,
// so global state didn't change.
if (++agentsWithRuntimeAndLogDomainsEnabled_ == 1) {
emitDebuggerSessionCreated();
}
} else if (domainStateChangedLocally) {
if (--agentsWithRuntimeAndLogDomainsEnabled_ == 0) {
emitDebuggerSessionDestroyed();
}
}
break;
}
case Domain::Network:
break;
case Domain::kMaxValue: {
throw std::logic_error("Unexpected kMaxValue domain value provided");
}
}
}
std::pair<bool, bool> RuntimeTarget::processDomainChange(
Domain domain,
bool enabled,
const RuntimeAgent& notifyingAgent) {
bool domainHadAgentsBefore = !agentsByEnabledDomain_[domain].empty();
bool domainHasBeenEnabledBefore =
agentsByEnabledDomain_[domain].contains(&notifyingAgent);
if (enabled) {
agentsByEnabledDomain_[domain].insert(&notifyingAgent);
} else {
agentsByEnabledDomain_[domain].erase(&notifyingAgent);
}
threadSafeDomainStatus_[domain] = !agentsByEnabledDomain_[domain].empty();
bool domainHasAgentsAfter = !agentsByEnabledDomain_[domain].empty();
return {
domainHasBeenEnabledBefore ^ enabled,
domainHadAgentsBefore ^ domainHasAgentsAfter,
};
}
bool RuntimeTarget::isDomainEnabled(Domain domain) const {
return threadSafeDomainStatus_[domain];
}
RuntimeTargetController::RuntimeTargetController(RuntimeTarget& target)
: target_(target) {}
void RuntimeTargetController::installBindingHandler(
const std::string& bindingName) {
target_.installBindingHandler(bindingName);
}
void RuntimeTargetController::enableSamplingProfiler() {
target_.enableSamplingProfiler();
}
void RuntimeTargetController::disableSamplingProfiler() {
target_.disableSamplingProfiler();
}
tracing::RuntimeSamplingProfile
RuntimeTargetController::collectSamplingProfile() {
return target_.collectSamplingProfile();
}
void RuntimeTargetController::notifyDomainStateChanged(
Domain domain,
bool enabled,
const RuntimeAgent& notifyingAgent) {
target_.notifyDomainStateChanged(domain, enabled, notifyingAgent);
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,361 @@
/*
* 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 "ConsoleMessage.h"
#include "EnumArray.h"
#include "ExecutionContext.h"
#include "InspectorInterfaces.h"
#include "RuntimeAgent.h"
#include "ScopedExecutor.h"
#include "StackTrace.h"
#include "WeakList.h"
#include <ReactCommon/RuntimeExecutor.h>
#include <jsinspector-modern/tracing/RuntimeSamplingProfile.h>
#include <jsinspector-modern/tracing/TraceRecordingState.h>
#include <memory>
#include <utility>
#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 RuntimeTracingAgent;
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,
RuntimeExecutor runtimeExecutor) = 0;
/**
* Called when the runtime intercepts a console API call. The target delegate
* should notify the frontend (via its agent delegates) of the message, and
* perform any buffering required for logging the message later (in the
* existing and/or new sessions).
*
* \note The method is called on the JS thread, and receives a valid reference
* to the current \c jsi::Runtime. The callee MAY use its own intrinsic
* Runtime reference, if it has one, without checking it for equivalence with
* the one provided here.
*/
virtual void addConsoleMessage(jsi::Runtime &runtime, ConsoleMessage message) = 0;
/**
* \returns true if the runtime supports reporting console API calls over CDP.
* \c addConsoleMessage MAY be called even if this method returns false.
*/
virtual bool supportsConsole() const = 0;
/**
* \returns an opaque representation of a stack trace. This may be passed back
* to the `RuntimeTargetDelegate` as part of `addConsoleMessage` or other APIs
* that report stack traces.
* \param framesToSkip The number of call frames to skip. The first call frame
* is the topmost (current) frame on the Runtime's call stack, which will
* typically be the (native) JSI HostFunction that called this method.
* \note The method is called on the JS thread, and receives a valid reference
* to the current \c jsi::Runtime. The callee MAY use its own intrinsic
* Runtime reference, if it has one, without checking it for equivalence with
* the one provided here.
*/
virtual std::unique_ptr<StackTrace> captureStackTrace(jsi::Runtime &runtime, size_t framesToSkip = 0) = 0;
/**
* Start sampling profiler.
*/
virtual void enableSamplingProfiler() = 0;
/**
* Stop sampling profiler.
*/
virtual void disableSamplingProfiler() = 0;
/**
* Return recorded sampling profile for the previous sampling session.
*/
virtual tracing::RuntimeSamplingProfile collectSamplingProfile() = 0;
/**
* \returns a JSON representation of the given stack trace, conforming to the
* @cdp Runtime.StackTrace type, if the runtime supports it. Otherwise,
* returns std::nullopt.
*/
virtual std::optional<folly::dynamic> serializeStackTrace(const StackTrace &stackTrace) = 0;
};
/**
* The limited interface that RuntimeTarget exposes to its connected agents.
*/
class RuntimeTargetController {
public:
enum class Domain { Log, Network, Runtime, kMaxValue };
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);
/**
* Notifies the target that an agent has received an enable or disable
* message for the given domain.
*/
void notifyDomainStateChanged(Domain domain, bool enabled, const RuntimeAgent &notifyingAgent);
/**
* Start sampling profiler for the corresponding RuntimeTarget.
*/
void enableSamplingProfiler();
/**
* Stop sampling profiler for the corresponding RuntimeTarget.
*/
void disableSamplingProfiler();
/**
* Return recorded sampling profile for the previous sampling session.
*/
tracing::RuntimeSamplingProfile collectSamplingProfile();
private:
RuntimeTarget &target_;
};
/**
* A Target corresponding to a JavaScript runtime.
*/
class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis<RuntimeTarget> {
public:
/**
* \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 Host).
* \param delegate The object that will receive events from this target. The
* caller is responsible for ensuring that the delegate outlives this object
* AND that it remains valid for as long as the JS runtime is executing any
* code, even if the \c RuntimeTarget itself is destroyed. The delegate SHOULD
* be the object that owns the underlying jsi::Runtime, if any.
* \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 HostTarget, or the RuntimeAgent is
* unregistered from its InstanceTarget (whichever happens first).
* \param channel A thread-safe channel forHostTargetDP messages to the
* frontend.
* \returns The new agent, or nullptr if the runtime is not debuggable.
*/
std::shared_ptr<RuntimeAgent> createAgent(const FrontendChannel &channel, SessionState &sessionState);
/**
* Creates a new RuntimeTracingAgent.
* This Agent is not owned by the RuntimeTarget. The Agent will be destroyed
* either before the RuntimeTarget is destroyed, as part of the RuntimeTarget
* unregistration in InstanceTarget, or at the end of the tracing session.
*
* \param state A reference to the state of the active trace recording.
*/
std::shared_ptr<RuntimeTracingAgent> createTracingAgent(tracing::TraceRecordingState &state);
/**
* Start sampling profiler for a particular JavaScript runtime.
*/
void enableSamplingProfiler();
/**
* Stop sampling profiler for a particular JavaScript runtime.
*/
void disableSamplingProfiler();
/**
* Return recorded sampling profile for the previous sampling session.
*/
tracing::RuntimeSamplingProfile collectSamplingProfile();
private:
using Domain = RuntimeTargetController::Domain;
/**
* 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 Host).
* \param delegate The object that will receive events from this target. The
* caller is responsible for ensuring that the delegate outlives this object
* AND that it remains valid for as long as the JS runtime is executing any
* code, even if the \c RuntimeTarget itself is destroyed. The delegate SHOULD
* be the object that owns the underlying jsi::Runtime, if any.
* \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(
ExecutionContextDescription executionContextDescription,
RuntimeTargetDelegate &delegate,
RuntimeExecutor jsExecutor);
const ExecutionContextDescription executionContextDescription_;
RuntimeTargetDelegate &delegate_;
RuntimeExecutor jsExecutor_;
WeakList<RuntimeAgent> agents_;
RuntimeTargetController controller_{*this};
/**
* Keeps track of the agents that have enabled various domains.
*/
EnumArray<Domain, std::unordered_set<const RuntimeAgent *>> agentsByEnabledDomain_;
/**
* For each Domain, contains true if the domain has been enabled by any
* active agent. Unlike agentsByEnabledDomain_, this is safe to read from any
* thread. \see isDomainEnabled.
*/
EnumArray<Domain, std::atomic<bool>> threadSafeDomainStatus_{};
/**
* The number of agents that currently have both the Log and Runtime domains
* enabled.
*/
size_t agentsWithRuntimeAndLogDomainsEnabled_{0};
/**
* This TracingAgent is owned by the InstanceTracingAgent, both are bound to
* the lifetime of their corresponding targets and the lifetime of the tracing
* session - HostTargetTraceRecording.
*/
std::weak_ptr<RuntimeTracingAgent> tracingAgent_;
/**
* 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);
/**
* Installs any global values we want to expose to framework/user JavaScript
* code.
*/
void installGlobals();
/**
* Install the console API handler.
*/
void installConsoleHandler();
/**
* Installs __DEBUGGER_SESSION_OBSERVER__ object on the JavaScript's global
* object, which later could be referenced from JavaScript side for
* determining the status of the debugger session.
*/
void installDebuggerSessionObserver();
/**
* Installs the private __NETWORK_REPORTER__ object on the Runtime's
* global object.
*/
void installNetworkReporterAPI();
/**
* Propagates the debugger session state change to the JavaScript via calling
* onStatusChange on __DEBUGGER_SESSION_OBSERVER__.
*/
void emitDebuggerSessionCreated();
/**
* Propagates the debugger session state change to the JavaScript via calling
* onStatusChange on __DEBUGGER_SESSION_OBSERVER__.
*/
void emitDebuggerSessionDestroyed();
/**
* \returns a globally unique ID for a network request.
* May be called from any thread as long as the RuntimeTarget is valid.
*/
std::string createNetworkRequestId();
/**
* Notifies the target that an agent has received an enable or disable
* message for the given domain.
*/
void notifyDomainStateChanged(Domain domain, bool enabled, const RuntimeAgent &notifyingAgent);
/**
* Processes the changes to the state of a given domain.
*
* Returns a pair of booleans:
* 1. Returns true, if an only if the given domain state changed locally,
* for a given session.
* 2. Returns true, if and only if the given domain state changed globally:
* when the given Agent is the only Agent that enabled given domain across
* sessions, or when the only Agent that had this domain enabled has
* disconnected.
*/
std::pair<bool, bool> processDomainChange(Domain domain, bool enabled, const RuntimeAgent &notifyingAgent);
/**
* Checks whether the given domain is enabled in at least one session
* that is currently connected. This may be called from any thread, with
* the caveat that the result can change at arbitrary times unless the caller
* is on the inspector thread.
*/
bool isDomainEnabled(Domain domain) const;
// 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,644 @@
/*
* 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/RuntimeTarget.h>
#include <jsinspector-modern/tracing/PerformanceTracer.h>
#include <reactperflogger/ReactPerfettoLogger.h>
#include <concepts>
#include <deque>
#include <string>
using namespace facebook::jsi;
using namespace std::string_literals;
namespace facebook::react::jsinspector_modern {
namespace {
inline std::optional<HighResTimeStamp> getTimeStampForPerfetto(
const std::optional<tracing::ConsoleTimeStampEntry>& consoleTimeStampEntry,
const HighResTimeStamp now) {
if (consoleTimeStampEntry) {
if (std::holds_alternative<HighResTimeStamp>(*consoleTimeStampEntry)) {
return std::get<HighResTimeStamp>(*consoleTimeStampEntry);
} else {
return std::nullopt;
}
} else {
return now;
}
}
struct ConsoleState {
/**
* https://console.spec.whatwg.org/#counting
*/
std::unordered_map<std::string, int> countMap;
/**
* https://console.spec.whatwg.org/#timing
*/
std::unordered_map<std::string, double> timerTable;
ConsoleState() = default;
ConsoleState(const ConsoleState&) = delete;
ConsoleState& operator=(const ConsoleState&) = delete;
ConsoleState(ConsoleState&&) = delete;
ConsoleState& operator=(ConsoleState&&) = delete;
~ConsoleState() = default;
};
/**
* JS `Object.create()`
*/
jsi::Object objectCreate(jsi::Runtime& runtime, jsi::Value prototype) {
auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object");
auto createFn = objectGlobal.getPropertyAsFunction(runtime, "create");
return createFn.callWithThis(runtime, objectGlobal, prototype)
.getObject(runtime);
}
bool toBoolean(jsi::Runtime& runtime, const jsi::Value& val) {
// Based on Operations.cpp:toBoolean in the Hermes VM.
if (val.isUndefined() || val.isNull()) {
return false;
}
if (val.isBool()) {
return val.getBool();
}
if (val.isNumber()) {
double m = val.getNumber();
return m != 0 && !std::isnan(m);
}
if (val.isSymbol() || val.isObject()) {
return true;
}
if (val.isString()) {
std::string s = val.getString(runtime).utf8(runtime);
return !s.empty();
}
assert(false && "All cases should be covered");
return false;
}
/**
* Get the current time in milliseconds as a double.
*/
double getTimestampMs() {
return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
std::chrono::system_clock::now().time_since_epoch())
.count();
}
template <typename T>
concept ConsoleMethodBody = std::invocable<
T,
jsi::Runtime& /*runtime*/,
const jsi::Value* /*args*/,
size_t /*count*/,
RuntimeTargetDelegate& /*runtimeTargetDelegate*/,
ConsoleState& /*state*/,
double /*timestampMs*/,
std::unique_ptr<StackTrace> /*stackTrace*/>;
template <typename T>
concept CallableAsHostFunction = std::invocable<
T,
jsi::Runtime& /*runtime*/,
const jsi::Value& /*thisVal*/,
const jsi::Value* /*args*/,
size_t /*count*/>;
void consoleCount(
jsi::Runtime& runtime,
const jsi::Value* args,
size_t count,
RuntimeTargetDelegate& runtimeTargetDelegate,
ConsoleState& state,
double timestampMs,
std::unique_ptr<StackTrace> stackTrace) {
std::string label = "default";
if (count > 0 && !args[0].isUndefined()) {
label = args[0].toString(runtime).utf8(runtime);
}
auto it = state.countMap.find(label);
if (it == state.countMap.end()) {
it = state.countMap.insert({label, 1}).first;
} else {
it->second++;
}
std::vector<jsi::Value> vec;
vec.emplace_back(
jsi::String::createFromUtf8(
runtime, label + ": "s + std::to_string(it->second)));
runtimeTargetDelegate.addConsoleMessage(
runtime,
{timestampMs,
ConsoleAPIType::kCount,
std::move(vec),
std::move(stackTrace)});
}
void consoleCountReset(
jsi::Runtime& runtime,
const jsi::Value* args,
size_t count,
RuntimeTargetDelegate& runtimeTargetDelegate,
ConsoleState& state,
double timestampMs,
std::unique_ptr<StackTrace> stackTrace) {
std::string label = "default";
if (count > 0 && !args[0].isUndefined()) {
label = args[0].toString(runtime).utf8(runtime);
}
auto it = state.countMap.find(label);
if (it == state.countMap.end()) {
std::vector<jsi::Value> vec;
vec.emplace_back(
jsi::String::createFromUtf8(
runtime, "Count for '"s + label + "' does not exist"));
runtimeTargetDelegate.addConsoleMessage(
runtime,
{timestampMs,
ConsoleAPIType::kWarning,
std::move(vec),
std::move(stackTrace)});
} else {
it->second = 0;
}
}
void consoleTime(
jsi::Runtime& runtime,
const jsi::Value* args,
size_t count,
RuntimeTargetDelegate& runtimeTargetDelegate,
ConsoleState& state,
double timestampMs,
std::unique_ptr<StackTrace> stackTrace) {
std::string label = "default";
if (count > 0 && !args[0].isUndefined()) {
label = args[0].toString(runtime).utf8(runtime);
}
auto it = state.timerTable.find(label);
if (it == state.timerTable.end()) {
state.timerTable.insert({label, timestampMs});
} else {
std::vector<jsi::Value> vec;
vec.emplace_back(
jsi::String::createFromUtf8(
runtime, "Timer '"s + label + "' already exists"));
runtimeTargetDelegate.addConsoleMessage(
runtime,
{timestampMs,
ConsoleAPIType::kWarning,
std::move(vec),
std::move(stackTrace)});
}
}
void consoleTimeEnd(
jsi::Runtime& runtime,
const jsi::Value* args,
size_t count,
RuntimeTargetDelegate& runtimeTargetDelegate,
ConsoleState& state,
double timestampMs,
std::unique_ptr<StackTrace> stackTrace) {
std::string label = "default";
if (count > 0 && !args[0].isUndefined()) {
label = args[0].toString(runtime).utf8(runtime);
}
auto it = state.timerTable.find(label);
if (it == state.timerTable.end()) {
std::vector<jsi::Value> vec;
vec.emplace_back(
jsi::String::createFromUtf8(
runtime, "Timer '"s + label + "' does not exist"));
runtimeTargetDelegate.addConsoleMessage(
runtime,
{timestampMs,
ConsoleAPIType::kWarning,
std::move(vec),
std::move(stackTrace)});
} else {
std::vector<jsi::Value> vec;
vec.emplace_back(
jsi::String::createFromUtf8(
runtime,
label + ": "s + std::to_string(timestampMs - it->second) + " ms"));
state.timerTable.erase(it);
runtimeTargetDelegate.addConsoleMessage(
runtime,
{timestampMs,
ConsoleAPIType::kTimeEnd,
std::move(vec),
std::move(stackTrace)});
}
}
void consoleTimeLog(
jsi::Runtime& runtime,
const jsi::Value* args,
size_t count,
RuntimeTargetDelegate& runtimeTargetDelegate,
ConsoleState& state,
double timestampMs,
std::unique_ptr<StackTrace> stackTrace) {
std::string label = "default";
if (count > 0 && !args[0].isUndefined()) {
label = args[0].toString(runtime).utf8(runtime);
}
auto it = state.timerTable.find(label);
if (it == state.timerTable.end()) {
std::vector<jsi::Value> vec;
vec.emplace_back(
jsi::String::createFromUtf8(
runtime, "Timer '"s + label + "' does not exist"));
runtimeTargetDelegate.addConsoleMessage(
runtime,
{timestampMs,
ConsoleAPIType::kWarning,
std::move(vec),
std::move(stackTrace)});
} else {
std::vector<jsi::Value> vec;
vec.emplace_back(
jsi::String::createFromUtf8(
runtime,
label + ": "s + std::to_string(timestampMs - it->second) + " ms"));
if (count > 1) {
for (size_t i = 1; i != count; ++i) {
vec.emplace_back(runtime, args[i]);
}
}
runtimeTargetDelegate.addConsoleMessage(
runtime,
{timestampMs,
ConsoleAPIType::kLog,
std::move(vec),
std::move(stackTrace)});
}
}
void consoleAssert(
jsi::Runtime& runtime,
const jsi::Value* args,
size_t count,
RuntimeTargetDelegate& runtimeTargetDelegate,
ConsoleState& /*state*/,
double timestampMs,
std::unique_ptr<StackTrace> stackTrace) {
if (count >= 1 && toBoolean(runtime, args[0])) {
return;
}
std::deque<jsi::Value> data;
if (count > 1) {
for (size_t i = 1; i != count; ++i) {
data.emplace_back(runtime, args[i]);
}
}
if (data.empty()) {
data.emplace_back(jsi::String::createFromUtf8(runtime, "Assertion failed"));
} else if (data.front().isString()) {
data.front() = jsi::String::createFromUtf8(
runtime,
"Assertion failed: "s + data.front().asString(runtime).utf8(runtime));
} else {
data.emplace_front(
jsi::String::createFromUtf8(runtime, "Assertion failed"));
}
runtimeTargetDelegate.addConsoleMessage(
runtime,
{timestampMs,
ConsoleAPIType::kAssert,
std::vector<jsi::Value>(
make_move_iterator(data.begin()), make_move_iterator(data.end())),
std::move(stackTrace)});
}
/**
* `console` methods that have no behaviour other than emitting a
* Runtime.consoleAPICalled message.
*/
#define FORWARDING_CONSOLE_METHOD(name, type) \
void console_##name( \
jsi::Runtime& runtime, \
const jsi::Value* args, \
size_t count, \
RuntimeTargetDelegate& runtimeTargetDelegate, \
ConsoleState& state, \
double timestampMs, \
std::unique_ptr<StackTrace> stackTrace) { \
std::vector<jsi::Value> argsVec; \
for (size_t i = 0; i != count; ++i) { \
argsVec.emplace_back(runtime, args[i]); \
} \
runtimeTargetDelegate.addConsoleMessage( \
runtime, \
{timestampMs, type, std::move(argsVec), std::move(stackTrace)}); \
}
#include "ForwardingConsoleMethods.def"
#undef FORWARDING_CONSOLE_METHOD
/*
* Attempt to call String() on the given value.
*/
std::optional<std::string> stringifyJsiValue(
const jsi::Value& value,
jsi::Runtime& runtime) {
auto String = runtime.global().getPropertyAsFunction(runtime, "String");
return String.call(runtime, value).asString(runtime).utf8(runtime);
};
/**
* Call innerFn and forward any arguments to the original console method
* named methodName, if possible.
*/
auto forwardToOriginalConsole(
std::shared_ptr<jsi::Object> originalConsole,
const char* methodName,
CallableAsHostFunction auto innerFn) {
return [originalConsole = std::move(originalConsole),
innerFn = std::move(innerFn),
methodName](
jsi::Runtime& runtime,
const jsi::Value& thisVal,
const jsi::Value* args,
size_t count) {
jsi::Value retVal = innerFn(runtime, thisVal, args, count);
if (originalConsole) {
auto val = originalConsole->getProperty(runtime, methodName);
if (val.isObject()) {
auto obj = val.getObject(runtime);
if (obj.isFunction(runtime)) {
auto func = obj.getFunction(runtime);
func.callWithThis(runtime, *originalConsole, args, count);
}
}
}
return std::move(retVal);
};
};
/**
* Recording a marker on a timeline of the Performance instrumentation.
* No actual logging is provided by definition.
* https://developer.mozilla.org/en-US/docs/Web/API/console/timeStamp_static
* https://developer.chrome.com/docs/devtools/performance/extension#inject_your_data_with_consoletimestamp
*/
void consoleTimeStamp(
jsi::Runtime& runtime,
const jsi::Value* arguments,
size_t argumentsCount) {
auto& performanceTracer = tracing::PerformanceTracer::getInstance();
if ((!performanceTracer.isTracing() && !ReactPerfettoLogger::isTracing()) ||
argumentsCount == 0) {
// If not tracing, just early return to avoid the cost of parsing.
return;
}
const jsi::Value& labelArgument = arguments[0];
std::string label;
if (labelArgument.isString()) {
label = labelArgument.asString(runtime).utf8(runtime);
} else {
auto maybeStringifiedLabel = stringifyJsiValue(labelArgument, runtime);
if (maybeStringifiedLabel) {
label = std::move(*maybeStringifiedLabel);
} else {
// Do not record this entry: unable to reliably stringify the label.
return;
}
}
auto now = HighResTimeStamp::now();
std::optional<tracing::ConsoleTimeStampEntry> start;
if (argumentsCount >= 2) {
const jsi::Value& startArgument = arguments[1];
if (startArgument.isNumber()) {
start =
HighResTimeStamp::fromDOMHighResTimeStamp(startArgument.asNumber());
} else if (startArgument.isString()) {
start = startArgument.asString(runtime).utf8(runtime);
} else if (startArgument.isUndefined()) {
start = now;
}
}
std::optional<tracing::ConsoleTimeStampEntry> end;
if (argumentsCount >= 3) {
const jsi::Value& endArgument = arguments[2];
if (endArgument.isNumber()) {
end = HighResTimeStamp::fromDOMHighResTimeStamp(endArgument.asNumber());
} else if (endArgument.isString()) {
end = endArgument.asString(runtime).utf8(runtime);
} else if (endArgument.isUndefined()) {
end = now;
}
}
std::optional<std::string> trackName;
std::optional<std::string> trackGroup;
std::optional<tracing::ConsoleTimeStampColor> color;
if (argumentsCount >= 4) {
const jsi::Value& trackNameArgument = arguments[3];
if (trackNameArgument.isString()) {
trackName = trackNameArgument.asString(runtime).utf8(runtime);
}
}
if (argumentsCount >= 5) {
const jsi::Value& trackGroupArgument = arguments[4];
if (trackGroupArgument.isString()) {
trackGroup = trackGroupArgument.asString(runtime).utf8(runtime);
}
}
if (argumentsCount >= 6) {
const jsi::Value& colorArgument = arguments[5];
if (colorArgument.isString()) {
color = tracing::getConsoleTimeStampColorFromString(
colorArgument.asString(runtime).utf8(runtime));
}
}
std::optional<folly::dynamic> detail;
if (performanceTracer.isTracing() && argumentsCount >= 7) {
const jsi::Value& detailArgument = arguments[6];
if (detailArgument.isObject()) {
detail =
tracing::getConsoleTimeStampDetailFromObject(runtime, detailArgument);
}
}
if (performanceTracer.isTracing()) {
performanceTracer.reportTimeStamp(
label, start, end, trackName, trackGroup, color, std::move(detail));
}
if (ReactPerfettoLogger::isTracing()) {
std::optional<HighResTimeStamp> perfettoStart =
getTimeStampForPerfetto(start, now);
std::optional<HighResTimeStamp> perfettoEnd =
getTimeStampForPerfetto(end, now);
if (perfettoStart && perfettoEnd) {
ReactPerfettoLogger::measure(
label, *perfettoStart, *perfettoEnd, trackName, trackGroup);
}
}
}
/*
* Installs console.timeStamp and manages the forwarding to the original console
* object, if available.
*/
void installConsoleTimeStamp(
jsi::Runtime& runtime,
std::shared_ptr<jsi::Object> originalConsole,
jsi::Object& consoleObject) {
consoleObject.setProperty(
runtime,
"timeStamp",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "timeStamp"),
0,
forwardToOriginalConsole(
originalConsole,
"timeStamp",
[](jsi::Runtime& runtime,
const jsi::Value& /*thisVal*/,
const jsi::Value* args,
size_t count) {
consoleTimeStamp(runtime, args, count);
return jsi::Value::undefined();
})));
}
} // namespace
void RuntimeTarget::installConsoleHandler() {
auto delegateSupportsConsole = delegate_.supportsConsole();
jsExecutor_([selfWeak = weak_from_this(),
selfExecutor = executorFromThis(),
delegateSupportsConsole](jsi::Runtime& runtime) {
jsi::Value consolePrototype = jsi::Value::null();
auto originalConsoleVal = runtime.global().getProperty(runtime, "console");
std::shared_ptr<jsi::Object> originalConsole;
if (originalConsoleVal.isObject()) {
originalConsole =
std::make_shared<jsi::Object>(originalConsoleVal.getObject(runtime));
consolePrototype = std::move(originalConsoleVal);
} else {
consolePrototype = jsi::Object(runtime);
}
auto console = objectCreate(runtime, std::move(consolePrototype));
auto state = std::make_shared<ConsoleState>();
/**
* Install a console method with the given name and body. The body receives
* the usual JSI host function parameters plus a ConsoleState reference, a
* reference to the RuntimeTargetDelegate for sending messages to the
* client, and the timestamp of the call. After the body runs (or is skipped
* due to RuntimeTarget having been destroyed), the method of the same name
* is also called on originalConsole (if it exists).
*/
auto installConsoleMethod = [&](const char* methodName,
ConsoleMethodBody auto body) {
console.setProperty(
runtime,
methodName,
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, methodName),
0,
forwardToOriginalConsole(
originalConsole,
methodName,
[body = std::move(body), state, selfWeak](
jsi::Runtime& runtime,
const jsi::Value& /*thisVal*/,
const jsi::Value* args,
size_t count) {
auto timestampMs = getTimestampMs();
tryExecuteSync(selfWeak, [&](auto& self) {
// Q: Why is it safe to use self->delegate_ here?
// A: Because the caller of
// InspectorTarget::registerRuntime is explicitly required
// to guarantee that the delegate not only outlives the
// target, but also outlives all JS code execution that
// occurs on the JS thread.
auto stackTrace = self.delegate_.captureStackTrace(
runtime, /* framesToSkip */ 1);
body(
runtime,
args,
count,
self.delegate_,
*state,
timestampMs,
std::move(stackTrace));
});
return jsi::Value::undefined();
})));
};
/**
* console.count
*/
installConsoleMethod("count", consoleCount);
/**
* console.countReset
*/
installConsoleMethod("countReset", consoleCountReset);
/**
* console.time
*/
installConsoleMethod("time", consoleTime);
/**
* console.timeEnd
*/
installConsoleMethod("timeEnd", consoleTimeEnd);
/**
* console.timeLog
*/
installConsoleMethod("timeLog", consoleTimeLog);
/**
* console.assert
*/
installConsoleMethod("assert", consoleAssert);
/**
* console.timeStamp
*/
installConsoleTimeStamp(runtime, originalConsole, console);
// Install forwarding console methods.
#define FORWARDING_CONSOLE_METHOD(name, type) \
installConsoleMethod(#name, console_##name);
#include "ForwardingConsoleMethods.def"
#undef FORWARDING_CONSOLE_METHOD
runtime.global().setProperty(runtime, "console", console);
if (delegateSupportsConsole) {
// NOTE: If the delegate doesn't report console support, we'll still
// install the console handler for consistency of the runtime environment,
// but not claim that it has full console support.
runtime.global().setProperty(
runtime, "__FUSEBOX_HAS_FULL_CONSOLE_SUPPORT__", true);
}
});
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,112 @@
/*
* 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/RuntimeTarget.h>
namespace facebook::react::jsinspector_modern {
void RuntimeTarget::installDebuggerSessionObserver() {
jsExecutor_([](jsi::Runtime& runtime) {
auto globalObj = runtime.global();
try {
auto observer = jsi::Object(runtime);
observer.setProperty(runtime, "hasActiveSession", jsi::Value(false));
auto setFunction = globalObj.getPropertyAsFunction(runtime, "Set");
auto set = setFunction.callAsConstructor(runtime);
observer.setProperty(runtime, "subscribers", set);
observer.setProperty(
runtime,
"onSessionStatusChange",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "onSessionStatusChange"),
1,
[](jsi::Runtime& onSessionStatusChangeRuntime,
const jsi::Value& /* onSessionStatusChangeThisVal */,
const jsi::Value* onSessionStatusChangeArgs,
size_t onSessionStatusChangeArgsCount) {
if (onSessionStatusChangeArgsCount != 1 ||
!onSessionStatusChangeArgs[0].isBool()) {
throw jsi::JSError(
onSessionStatusChangeRuntime,
"Invalid arguments: onSessionStatusChange expects 1 boolean argument");
}
bool updatedStatus = onSessionStatusChangeArgs[0].getBool();
auto observerInstanceFromOnSessionStatusChange =
onSessionStatusChangeRuntime.global().getPropertyAsObject(
onSessionStatusChangeRuntime,
"__DEBUGGER_SESSION_OBSERVER__");
auto subscribersToNotify =
observerInstanceFromOnSessionStatusChange
.getPropertyAsObject(
onSessionStatusChangeRuntime, "subscribers");
observerInstanceFromOnSessionStatusChange.setProperty(
onSessionStatusChangeRuntime,
"hasActiveSession",
updatedStatus);
if (subscribersToNotify
.getProperty(onSessionStatusChangeRuntime, "size")
.asNumber() == 0) {
return jsi::Value::undefined();
}
auto forEachSubscriber =
subscribersToNotify.getPropertyAsFunction(
onSessionStatusChangeRuntime, "forEach");
auto forEachSubscriberCallback =
jsi::Function::createFromHostFunction(
onSessionStatusChangeRuntime,
jsi::PropNameID::forAscii(
onSessionStatusChangeRuntime, "forEachCallback"),
1,
[updatedStatus](
jsi::Runtime& forEachCallbackRuntime,
const jsi::Value& /* forEachCallbackThisVal */,
const jsi::Value* forEachCallbackArgs,
size_t forEachCallbackArgsCount) {
if (forEachCallbackArgsCount < 1 ||
!forEachCallbackArgs[0].isObject() ||
!forEachCallbackArgs[0]
.getObject(forEachCallbackRuntime)
.isFunction(forEachCallbackRuntime)) {
throw jsi::JSError(
forEachCallbackRuntime,
"Invalid arguments: forEachSubscriberCallback expects function as a first argument");
}
forEachCallbackArgs[0]
.getObject(forEachCallbackRuntime)
.asFunction(forEachCallbackRuntime)
.call(forEachCallbackRuntime, updatedStatus);
return jsi::Value::undefined();
});
forEachSubscriber.callWithThis(
onSessionStatusChangeRuntime,
subscribersToNotify,
forEachSubscriberCallback);
return jsi::Value::undefined();
}));
globalObj.setProperty(runtime, "__DEBUGGER_SESSION_OBSERVER__", observer);
} catch (jsi::JSError&) {
// Suppress any errors, they should not be visible to the user
// and should not affect runtime.
}
});
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,98 @@
/*
* 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/InspectorFlags.h>
#include <jsinspector-modern/RuntimeTarget.h>
#include <jsinspector-modern/network/NetworkHandler.h>
#include <react/utils/Uuid.h>
using namespace facebook::jsi;
using namespace std::string_literals;
namespace facebook::react::jsinspector_modern {
namespace {
/**
* JS `Object.create()`
*/
Object objectCreate(Runtime& runtime, Value prototype) {
auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object");
auto createFn = objectGlobal.getPropertyAsFunction(runtime, "create");
return createFn.callWithThis(runtime, objectGlobal, prototype)
.getObject(runtime);
}
/**
* JS `Object.freeze()`
*/
Object objectFreeze(Runtime& runtime, Object object) {
auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object");
auto freezeFn = objectGlobal.getPropertyAsFunction(runtime, "freeze");
return freezeFn.callWithThis(runtime, objectGlobal, object)
.getObject(runtime);
}
} // namespace
void RuntimeTarget::installNetworkReporterAPI() {
if (!InspectorFlags::getInstance().getNetworkInspectionEnabled()) {
return;
}
auto jsiCreateDevToolsRequestId = [selfWeak = weak_from_this()](
Runtime& runtime,
const Value& /*thisVal*/,
const Value* /*args*/,
size_t /*count*/) -> Value {
std::optional<std::string> devToolsRequestId;
tryExecuteSync(selfWeak, [&](RuntimeTarget& self) {
devToolsRequestId = self.createNetworkRequestId();
if (self.isDomainEnabled(Domain::Network)) {
// Q: Why is it safe to use self.delegate_ here?
// A: Because the caller of InspectorTarget::registerRuntime
// is explicitly required to guarantee that the delegate not
// only outlives the target, but also outlives all JS code
// execution that occurs on the JS thread.
auto stackTrace = self.delegate_.captureStackTrace(runtime);
auto cdpStackTrace = self.delegate_.serializeStackTrace(*stackTrace);
if (cdpStackTrace) {
NetworkHandler::getInstance().recordRequestInitiatorStack(
*devToolsRequestId, std::move(*cdpStackTrace));
}
}
});
if (!devToolsRequestId) {
throw JSError(runtime, "React Native Runtime is shutting down");
}
return String::createFromUtf8(runtime, *devToolsRequestId);
};
jsExecutor_([selfWeak = weak_from_this(),
selfExecutor = executorFromThis(),
jsiCreateDevToolsRequestId =
std::move(jsiCreateDevToolsRequestId)](Runtime& runtime) {
auto globalObj = runtime.global();
auto networkReporterApi = objectCreate(runtime, nullptr);
networkReporterApi.setProperty(
runtime,
"createDevToolsRequestId",
Function::createFromHostFunction(
runtime,
PropNameID::forAscii(runtime, "createDevToolsRequestId"),
0,
jsiCreateDevToolsRequestId));
networkReporterApi = objectFreeze(runtime, std::move(networkReporterApi));
globalObj.setProperty(runtime, "__NETWORK_REPORTER__", networkReporterApi);
});
}
std::string RuntimeTarget::createNetworkRequestId() {
return generateRandomUuidString();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,122 @@
/*
* 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 <react/utils/OnScopeExit.h>
#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_;
};
/**
* Synchronously executes a callback if the given object is still alive,
* and keeps the object alive at least until the callback returns, without
* moving ownership of the object itself across threads.
*
* The caller is responsible for all thread safety concerns outside of the
* lifetime of the object itself (e.g. the safety of calling particular methods
* on the object).
*/
template <typename ExecutorEnabledType>
requires std::derived_from<ExecutorEnabledType, EnableExecutorFromThis<ExecutorEnabledType>>
static void tryExecuteSync(std::weak_ptr<ExecutorEnabledType> selfWeak, std::invocable<ExecutorEnabledType &> auto func)
{
if (auto self = selfWeak.lock()) {
auto selfExecutor = self->executorFromThis();
OnScopeExit onScopeExit{[self, selfExecutor = std::move(selfExecutor)]() {
// To ensure we never destroy `self` on the wrong thread, send
// our shared_ptr back to the correct executor.
selfExecutor([self = std::move(self)](auto &) { (void)self; });
}};
func(*self);
}
}
} // namespace facebook::react::jsinspector_modern

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 "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 isDebuggerDomainEnabled{false};
bool isLogDomainEnabled{false};
bool isReactNativeApplicationDomainEnabled{false};
bool isRuntimeDomainEnabled{false};
bool isNetworkDomainEnabled{false};
/**
* Whether the Trace Recording was initialized via CDP Tracing.start method
* and not finished yet with Tracing.stop.
*/
bool hasPendingTraceRecording{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;
/**
* Messages logged through the HostAgent::sendConsoleMessage and
* InstanceAgent::sendConsoleMessage utilities that have not yet been sent to
* the frontend.
* \note This is unrelated to RuntimeTarget's user-facing console API
* implementation, which depends on access to JSI and support from the
* RuntimeTargetDelegate.
*/
std::vector<SimpleConsoleMessage> pendingSimpleConsoleMessages;
/**
* 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,40 @@
/*
* 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 <memory>
namespace facebook::react::jsinspector_modern {
/**
* An opaque representation of a stack trace.
*/
class StackTrace {
public:
/**
* Constructs an empty stack trace.
*/
static inline std::unique_ptr<StackTrace> empty()
{
return std::make_unique<StackTrace>();
}
/**
* Constructs an empty stack trace.
*/
StackTrace() = default;
StackTrace(const StackTrace &) = delete;
StackTrace &operator=(const StackTrace &) = delete;
StackTrace(StackTrace &&) = delete;
StackTrace &operator=(StackTrace &&) = delete;
virtual ~StackTrace() = default;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,135 @@
/*
* 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 "TracingAgent.h"
#include <jsinspector-modern/tracing/PerformanceTracer.h>
#include <jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h>
#include <jsinspector-modern/tracing/TraceEventSerializer.h>
#include <jsinspector-modern/tracing/TraceRecordingStateSerializer.h>
#include <jsinspector-modern/tracing/TracingMode.h>
namespace facebook::react::jsinspector_modern {
namespace {
/**
* Threshold for the size Trace Event chunk, that will be flushed out with
* Tracing.dataCollected event.
*/
const uint16_t TRACE_EVENT_CHUNK_SIZE = 1000;
/**
* The maximum number of ProfileChunk trace events
* that will be sent in a single CDP Tracing.dataCollected message.
* TODO(T219394401): Increase the size once we manage the queue on OkHTTP
side
* properly and avoid WebSocket disconnections when sending a message larger
* than 16MB.
*/
const uint16_t PROFILE_TRACE_EVENT_CHUNK_SIZE = 1;
} // namespace
TracingAgent::TracingAgent(
FrontendChannel frontendChannel,
SessionState& sessionState,
HostTargetController& hostTargetController)
: frontendChannel_(std::move(frontendChannel)),
sessionState_(sessionState),
hostTargetController_(hostTargetController) {}
TracingAgent::~TracingAgent() {
// Agents are owned by the session. If the agent is destroyed, it means that
// the session was destroyed. We should stop pending recording.
if (sessionState_.hasPendingTraceRecording) {
hostTargetController_.stopTracing();
}
}
bool TracingAgent::handleRequest(const cdp::PreparsedRequest& req) {
if (req.method == "Tracing.start") {
auto& inspector = getInspectorInstance();
if (inspector.getSystemState().registeredHostsCount > 1) {
frontendChannel_(
cdp::jsonError(
req.id,
cdp::ErrorCode::InternalError,
"The Tracing domain is unavailable when multiple React Native hosts are registered."));
return true;
}
if (sessionState_.isDebuggerDomainEnabled) {
frontendChannel_(
cdp::jsonError(
req.id,
cdp::ErrorCode::InternalError,
"Debugger domain is expected to be disabled before starting Tracing"));
return true;
}
bool didNotHaveAlreadyRunningRecording =
hostTargetController_.startTracing(tracing::Mode::CDP);
if (!didNotHaveAlreadyRunningRecording) {
frontendChannel_(
cdp::jsonError(
req.id,
cdp::ErrorCode::InvalidRequest,
"Tracing has already been started"));
return true;
}
sessionState_.hasPendingTraceRecording = true;
frontendChannel_(cdp::jsonResult(req.id));
return true;
} else if (req.method == "Tracing.end") {
// @cdp Tracing.end support is experimental.
auto state = hostTargetController_.stopTracing();
sessionState_.hasPendingTraceRecording = false;
// Send response to Tracing.end request.
frontendChannel_(cdp::jsonResult(req.id));
emitTraceRecording(std::move(state));
return true;
}
return false;
}
void TracingAgent::emitExternalTraceRecording(
tracing::TraceRecordingState traceRecording) const {
frontendChannel_(
cdp::jsonNotification("ReactNativeApplication.traceRequested"));
emitTraceRecording(std::move(traceRecording));
}
void TracingAgent::emitTraceRecording(
tracing::TraceRecordingState traceRecording) const {
auto dataCollectedCallback = [this](folly::dynamic&& eventsChunk) {
frontendChannel_(
cdp::jsonNotification(
"Tracing.dataCollected",
folly::dynamic::object("value", std::move(eventsChunk))));
};
tracing::TraceRecordingStateSerializer::emitAsDataCollectedChunks(
std::move(traceRecording),
dataCollectedCallback,
TRACE_EVENT_CHUNK_SIZE,
PROFILE_TRACE_EVENT_CHUNK_SIZE);
frontendChannel_(
cdp::jsonNotification(
"Tracing.tracingComplete",
folly::dynamic::object("dataLossOccurred", false)));
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,65 @@
/*
* 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 "HostTarget.h"
#include "InspectorInterfaces.h"
#include <jsinspector-modern/cdp/CdpJson.h>
#include <jsinspector-modern/tracing/Timing.h>
#include <react/timing/primitives.h>
namespace facebook::react::jsinspector_modern {
/**
* Provides an agent for handling CDP's Tracing.start, Tracing.stop.
*/
class TracingAgent {
public:
/**
* \param frontendChannel A channel used to send responses to the
* frontend.
* \param sessionState The state of the session that created this agent.
* \param hostTargetController An interface to the HostTarget that this agent
* is attached to. The caller is responsible for ensuring that the
* HostTargetDelegate and underlying HostTarget both outlive the agent.
*/
TracingAgent(FrontendChannel frontendChannel, SessionState &sessionState, HostTargetController &hostTargetController);
~TracingAgent();
/**
* 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);
/**
* Emits the Trace Recording that was stashed externally by the HostTarget.
*/
void emitExternalTraceRecording(tracing::TraceRecordingState traceRecording) const;
private:
/**
* A channel used to send responses and events to the frontend.
*/
FrontendChannel frontendChannel_;
SessionState &sessionState_;
HostTargetController &hostTargetController_;
/**
* Emits the captured Trace Recording state in a series of
* Tracing.dataCollected events, followed by a Tracing.tracingComplete event.
*/
void emitTraceRecording(tracing::TraceRecordingState traceRecording) const;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,59 @@
/*
* 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> & /*unused*/) const noexcept
{
return true;
}
constexpr bool operator!=(const UniqueMonostate<key> & /*unused*/) const noexcept
{
return false;
}
constexpr bool operator<(const UniqueMonostate<key> & /*unused*/) const noexcept
{
return false;
}
constexpr bool operator>(const UniqueMonostate<key> & /*unused*/) const noexcept
{
return false;
}
constexpr bool operator<=(const UniqueMonostate<key> & /*unused*/) const noexcept
{
return true;
}
constexpr bool operator>=(const UniqueMonostate<key> & /*unused*/) 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> & /*unused*/) const noexcept
{
return key;
}
};
} // namespace std

View File

@@ -0,0 +1,56 @@
/*
* 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 <stdexcept>
#include <vector>
namespace facebook::react::jsinspector_modern {
/**
* Takes a vector of bytes representing a fragment of a UTF-8 string, and
* removes the minimum number (0-3) of trailing bytes so that the remainder is
* valid UTF-8. Useful for slicing binary data into UTF-8 strings.
*
* \param buffer Buffer to operate on - will be resized if necessary.
*/
inline void truncateToValidUTF8(std::vector<char> &buffer)
{
const auto length = buffer.size();
// Ensure we don't cut a UTF-8 code point in the middle by removing any
// trailing bytes representing an incomplete UTF-8 code point.
// If the last byte is a UTF-8 first byte or continuation byte (topmost bit
// is 1) (otherwise the last char is ASCII and we don't need to do
// anything).
if (length > 0 && (buffer[length - 1] & 0b10000000) == 0b10000000) {
int continuationBytes = 0;
// Find the first byte of the UTF-8 code point (topmost bits 11) and count
// the number of continuation bytes following it.
while ((buffer[length - continuationBytes - 1] & 0b11000000) != 0b11000000) {
continuationBytes++;
if (continuationBytes > 3 || continuationBytes >= length - 1) {
throw std::runtime_error("Invalid UTF-8 sequence");
}
}
char firstByteOfSequence = buffer[length - continuationBytes - 1];
// Check for the special case that our original cut point was at the end
// of a UTF-8 code-point, and therefore already valid. This will be the
// case if the first byte indicates continuationBytes continuation bytes
// should follow, i.e. its top bits are (1+continuationBytes) 1's followed
// by a 0.
char mask = static_cast<char>(0b11111000 << (3 - continuationBytes));
char expectedBitsAfterMask = static_cast<char>(mask << 1);
if (continuationBytes == 0 || (firstByteOfSequence & mask) != expectedBitsAfterMask) {
// Remove the trailing continuation bytes, if any, and the first byte.
buffer.resize(length - (continuationBytes + 1));
}
}
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,83 @@
/*
* 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,72 @@
/*
* 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.
*/
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.
* This method must be called on the inspector queue, and the
* WebSocketDelegate may not be destroyed while it is executing.
* \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.
* This method must be called on the inspector queue, and the
* WebSocketDelegate may not be destroyed while it is executing.
* \param message Message received, in UTF-8 encoding.
*/
virtual void didReceiveMessage(std::string_view message) = 0;
/**
* Called when the socket has been opened.
* This method must be called on the inspector queue, and the
* WebSocketDelegate may not be destroyed while it is executing.
*/
virtual void didOpen() = 0;
/**
* Called when the socket has been closed. The call is not required if
* didFailWithError was called instead.
* This method must be called on the inspector queue, and the
* WebSocketDelegate may not be destroyed while it is executing.
*/
virtual void didClose() = 0;
};
} // namespace facebook::react::jsinspector_modern

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)
include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
add_compile_options(
-fexceptions
-std=c++20
-Wall
-Wpedantic)
file(GLOB jsinspector_cdp_SRC CONFIGURE_DEPENDS *.cpp)
add_library(jsinspector_cdp OBJECT ${jsinspector_cdp_SRC})
target_merge_so(jsinspector_cdp)
target_include_directories(jsinspector_cdp PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(jsinspector_cdp
folly_runtime
)

View File

@@ -0,0 +1,72 @@
/*
* 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 "CdpJson.h"
#include <folly/dynamic.h>
#include <folly/json.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") != 0u ? 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);
}
std::string jsonError(
std::optional<RequestId> id,
ErrorCode code,
std::optional<std::string> message) {
auto dynamicError = folly::dynamic::object("code", static_cast<int>(code));
if (message) {
dynamicError("message", *message);
}
return folly::toJson(
(id ? folly::dynamic::object("id", *id)
: folly::dynamic::object("id", nullptr))(
"error", std::move(dynamicError)));
}
std::string jsonResult(RequestId id, const folly::dynamic& result) {
return folly::toJson(folly::dynamic::object("id", id)("result", result));
}
std::string jsonNotification(
const std::string& method,
std::optional<folly::dynamic> params) {
auto dynamicNotification = folly::dynamic::object("method", method);
if (params) {
dynamicNotification("params", *params);
}
return folly::toJson(std::move(dynamicNotification));
}
std::string jsonRequest(
RequestId id,
const std::string& method,
std::optional<folly::dynamic> params) {
auto dynamicRequest = folly::dynamic::object("id", id)("method", method);
if (params) {
dynamicRequest("params", *params);
}
return folly::toJson(std::move(dynamicRequest));
}
} // namespace facebook::react::jsinspector_modern::cdp

View File

@@ -0,0 +1,129 @@
/*
* 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::cdp {
using RequestId = long long;
/**
* Error codes to be used in CDP responses.
* https://www.jsonrpc.org/specification#error_object
*/
enum class ErrorCode {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603
/* -32000 to -32099: Implementation-defined server errors. */
};
/**
* 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;
/**
* Helper functions for creating CDP (loosely JSON-RPC) messages of various
* types, returning a JSON string ready for sending over the wire.
*/
/**
* Returns a JSON-formatted string representing an error.
*
* {"id": <id>, "error": { "code": <cdp error code>, "message": <message> }}
*
* \param id Request ID. Mandatory, null only if the request omitted it or
* could not be parsed.
* \param code Integer code from cdp::ErrorCode.
* \param message Optional, brief human-readable error message.
*/
std::string jsonError(std::optional<RequestId> id, ErrorCode code, std::optional<std::string> message = std::nullopt);
/**
* Returns a JSON-formatted string representing a successful response.
*
* {"id": <id>, "result": <result>}
*
* \param id The id of the request that this response corresponds to.
* \param result Result payload, defaulting to {}.
*/
std::string jsonResult(RequestId id, const folly::dynamic &result = folly::dynamic::object());
/**
* Returns a JSON-formatted string representing a unilateral notification.
*
* {"method": <method>, "params": <params>}
*
* \param method Notification (aka "event") method.
* \param params Optional payload object.
*/
std::string jsonNotification(const std::string &method, std::optional<folly::dynamic> params = std::nullopt);
/**
* Returns a JSON-formatted string representing a request.
*
* {"id": <id>, "method": <method>, "params": <params>}
*
* \param id Request ID.
* \param method Requested method.
* \param params Optional payload object.
*/
std::string jsonRequest(RequestId id, const std::string &method, std::optional<folly::dynamic> params = std::nullopt);
} // namespace facebook::react::jsinspector_modern::cdp

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.
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
header_search_paths = []
if ENV['USE_FRAMEWORKS']
header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../..\""
end
header_dir = 'jsinspector-modern/cdp'
module_name = "jsinspector_moderncdp"
Pod::Spec.new do |s|
s.name = "React-jsinspectorcdp"
s.version = version
s.summary = "Common helper functions for working with CDP messages in jsinspector-modern"
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 = podspec_sources("*.{cpp,h}", "*.h")
s.header_dir = header_dir
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"DEFINES_MODULE" => "YES"}
resolve_use_frameworks(s, header_mappings_dir: "../..", module_name: module_name)
add_rn_third_party_dependencies(s)
add_rncore_dependency(s)
end

View File

@@ -0,0 +1,77 @@
/*
* 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 "BoundedRequestBuffer.h"
namespace facebook::react::jsinspector_modern {
bool BoundedRequestBuffer::put(
const std::string& requestId,
std::string_view data,
bool base64Encoded) noexcept {
if (data.size() > REQUEST_BUFFER_MAX_SIZE_BYTES) {
return false;
}
// Remove existing request with the same ID, if any
if (auto it = responses_.find(requestId); it != responses_.end()) {
currentSize_ -= it->second->data.size();
responses_.erase(it);
// Update order: remove requestId from deque
for (auto orderIt = order_.begin(); orderIt != order_.end(); ++orderIt) {
if (*orderIt == requestId) {
order_.erase(orderIt);
break;
}
}
}
// Evict oldest requests if necessary to make space
while (currentSize_ + data.size() > REQUEST_BUFFER_MAX_SIZE_BYTES &&
!order_.empty()) {
const auto& oldestId = order_.front();
auto it = responses_.find(oldestId);
if (it != responses_.end()) {
currentSize_ -= it->second->data.size();
responses_.erase(it);
}
order_.pop_front();
}
// If still no space, reject the new data (this should not be reached)
if (currentSize_ + data.size() > REQUEST_BUFFER_MAX_SIZE_BYTES) {
return false;
}
currentSize_ += data.size();
// `data` is copied at the point of insertion
responses_.emplace(
requestId,
std::make_shared<ResponseBody>(ResponseBody{
.data = std::string(data), .base64Encoded = base64Encoded}));
order_.push_back(requestId);
return true;
}
std::shared_ptr<const BoundedRequestBuffer::ResponseBody>
BoundedRequestBuffer::get(const std::string& requestId) const {
auto it = responses_.find(requestId);
if (it != responses_.end()) {
return it->second;
}
return nullptr;
}
void BoundedRequestBuffer::clear() {
responses_.clear();
order_.clear();
currentSize_ = 0;
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,63 @@
/*
* 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 <deque>
#include <memory>
#include <string>
#include <unordered_map>
namespace facebook::react::jsinspector_modern {
/**
* Maximum memory size (in bytes) to store buffered text and image request
* bodies.
*/
constexpr size_t REQUEST_BUFFER_MAX_SIZE_BYTES = 100 * 1024 * 1024; // 100MB
/**
* A class to store network response previews keyed by requestId, with a fixed
* memory limit. Evicts oldest responses when memory is exceeded.
*/
class BoundedRequestBuffer {
public:
struct ResponseBody {
std::string data;
bool base64Encoded;
};
/**
* Store a response preview with the given requestId and data.
* If adding the data exceeds the memory limit, removes oldest requests until
* there is enough space or the buffer is empty.
* \param requestId Unique identifier for the request.
* \param data The request preview data (e.g. text or image body).
* \param base64Encoded True if the data is base64-encoded, false otherwise.
* \return True if the response body was stored, false otherwise.
*/
bool put(const std::string &requestId, std::string_view data, bool base64Encoded) noexcept;
/**
* Retrieve a response preview by requestId.
* \param requestId The unique identifier for the request.
* \return A shared pointer to the request data if found, otherwise nullptr.
*/
std::shared_ptr<const ResponseBody> get(const std::string &requestId) const;
/**
* Remove all entries from the buffer.
*/
void clear();
private:
std::unordered_map<std::string, std::shared_ptr<const ResponseBody>> responses_;
std::deque<std::string> order_;
size_t currentSize_ = 0;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,27 @@
# 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)
include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
add_compile_options(
-fexceptions
-std=c++20
-Wall
-Wpedantic)
file(GLOB jsinspector_network_SRC CONFIGURE_DEPENDS *.cpp)
add_library(jsinspector_network OBJECT ${jsinspector_network_SRC})
target_merge_so(jsinspector_network)
target_include_directories(jsinspector_network PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(jsinspector_network
folly_runtime
glog
jsinspector_cdp)

View File

@@ -0,0 +1,167 @@
/*
* 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 "CdpNetwork.h"
#include "HttpUtils.h"
namespace facebook::react::jsinspector_modern::cdp::network {
namespace {
folly::dynamic headersToDynamic(const Headers& headers) {
folly::dynamic result = folly::dynamic::object;
for (const auto& [key, value] : headers) {
result[key] = value;
}
return result;
}
} // namespace
folly::dynamic Request::toDynamic() const {
folly::dynamic result = folly::dynamic::object;
result["url"] = url;
result["method"] = method;
result["headers"] = headersToDynamic(headers);
result["postData"] = postData.value_or("");
return result;
}
/* static */ Response Response::fromInputParams(
const std::string& url,
uint16_t status,
const Headers& headers,
int encodedDataLength) {
return {
.url = url,
.status = status,
.statusText = httpReasonPhrase(status),
.headers = headers,
.mimeType = mimeTypeFromHeaders(headers),
.encodedDataLength = encodedDataLength,
};
}
folly::dynamic Response::toDynamic() const {
folly::dynamic result = folly::dynamic::object;
result["url"] = url;
result["status"] = status;
result["statusText"] = statusText;
result["headers"] = headersToDynamic(headers);
result["mimeType"] = mimeType;
result["encodedDataLength"] = encodedDataLength;
return result;
}
folly::dynamic RequestWillBeSentParams::toDynamic() const {
folly::dynamic params = folly::dynamic::object;
params["requestId"] = requestId;
params["loaderId"] = loaderId;
params["documentURL"] = documentURL;
params["request"] = request.toDynamic();
params["timestamp"] = timestamp;
params["wallTime"] = wallTime;
params["initiator"] = initiator;
params["redirectHasExtraInfo"] = redirectResponse.has_value();
if (redirectResponse.has_value()) {
params["redirectResponse"] = redirectResponse->toDynamic();
}
return params;
}
folly::dynamic RequestWillBeSentExtraInfoParams::toDynamic() const {
folly::dynamic params = folly::dynamic::object;
params["requestId"] = requestId;
params["associatedCookies"] = folly::dynamic::array;
params["headers"] = headersToDynamic(headers);
params["connectTiming"] =
folly::dynamic::object("requestTime", connectTiming.requestTime);
return params;
}
folly::dynamic ResponseReceivedParams::toDynamic() const {
folly::dynamic params = folly::dynamic::object;
params["requestId"] = requestId;
params["loaderId"] = loaderId;
params["timestamp"] = timestamp;
params["type"] = type;
params["response"] = response.toDynamic();
params["hasExtraInfo"] = hasExtraInfo;
return params;
}
folly::dynamic DataReceivedParams::toDynamic() const {
folly::dynamic params = folly::dynamic::object;
params["requestId"] = requestId;
params["timestamp"] = timestamp;
params["dataLength"] = dataLength;
params["encodedDataLength"] = encodedDataLength;
return params;
}
folly::dynamic LoadingFailedParams::toDynamic() const {
folly::dynamic params = folly::dynamic::object;
params["requestId"] = requestId;
params["timestamp"] = timestamp;
params["type"] = type;
params["errorText"] = errorText;
params["canceled"] = canceled;
return params;
}
folly::dynamic LoadingFinishedParams::toDynamic() const {
folly::dynamic params = folly::dynamic::object;
params["requestId"] = requestId;
params["timestamp"] = timestamp;
params["encodedDataLength"] = encodedDataLength;
return params;
}
std::string resourceTypeFromMimeType(const std::string& mimeType) {
if (mimeType.find("image/") == 0) {
return "Image";
}
if (mimeType.find("video/") == 0 || mimeType.find("audio/") == 0) {
return "Media";
}
if (mimeType == "application/javascript" || mimeType == "text/javascript" ||
mimeType == "application/x-javascript") {
return "Script";
}
if (mimeType == "application/json" || mimeType.find("application/xml") == 0 ||
mimeType == "text/xml") {
// Assume XHR for JSON/XML types
return "XHR";
}
return "Other";
}
} // namespace facebook::react::jsinspector_modern::cdp::network

View File

@@ -0,0 +1,146 @@
/*
* 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 <string>
// Data containers for CDP Network domain types, supporting serialization to
// folly::dynamic objects.
namespace facebook::react::jsinspector_modern::cdp::network {
using Headers = std::map<std::string, std::string>;
/**
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Request
*/
struct Request {
std::string url;
std::string method;
Headers headers;
std::optional<std::string> postData;
folly::dynamic toDynamic() const;
};
/**
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Response
*/
struct Response {
std::string url;
uint16_t status;
std::string statusText;
Headers headers;
std::string mimeType;
int encodedDataLength;
/**
* Convenience function to construct a `Response` from the generic
* `ResponseInfo` input object.
*/
static Response
fromInputParams(const std::string &url, uint16_t status, const Headers &headers, int encodedDataLength);
folly::dynamic toDynamic() const;
};
/**
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ConnectTiming
*/
struct ConnectTiming {
double requestTime;
};
/**
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-requestWillBeSent
*/
struct RequestWillBeSentParams {
std::string requestId;
std::string loaderId;
std::string documentURL;
Request request;
double timestamp;
double wallTime;
folly::dynamic initiator;
bool redirectHasExtraInfo;
std::optional<Response> redirectResponse;
folly::dynamic toDynamic() const;
};
/**
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-requestWillBeSentExtraInfo
*/
struct RequestWillBeSentExtraInfoParams {
std::string requestId;
Headers headers;
ConnectTiming connectTiming;
folly::dynamic toDynamic() const;
};
/**
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-responseReceived
*/
struct ResponseReceivedParams {
std::string requestId;
std::string loaderId;
double timestamp;
std::string type;
Response response;
bool hasExtraInfo;
folly::dynamic toDynamic() const;
};
/**
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-dataReceived
*/
struct DataReceivedParams {
std::string requestId;
double timestamp;
int dataLength;
int encodedDataLength;
folly::dynamic toDynamic() const;
};
/**
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-loadingFailed
*/
struct LoadingFailedParams {
std::string requestId;
double timestamp;
std::string type;
std::string errorText;
bool canceled;
folly::dynamic toDynamic() const;
};
/**
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-loadingFinished
*/
struct LoadingFinishedParams {
std::string requestId;
double timestamp;
int encodedDataLength;
folly::dynamic toDynamic() const;
};
/**
* Get the CDP `ResourceType` for a given MIME type.
*
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType
*/
std::string resourceTypeFromMimeType(const std::string &mimeType);
} // namespace facebook::react::jsinspector_modern::cdp::network

View File

@@ -0,0 +1,174 @@
/*
* 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 "HttpUtils.h"
#include <algorithm>
namespace facebook::react::jsinspector_modern {
std::string httpReasonPhrase(uint16_t status) {
switch (status) {
case 100:
return "Continue";
case 101:
return "Switching Protocols";
case 102:
return "Processing";
case 103:
return "Early Hints";
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Non-Authoritative Information";
case 204:
return "No Content";
case 205:
return "Reset Content";
case 206:
return "Partial Content";
case 207:
return "Multi-Status";
case 208:
return "Already Reported";
case 226:
return "IM Used";
case 300:
return "Multiple Choices";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 303:
return "See Other";
case 304:
return "Not Modified";
case 305:
return "Use Proxy";
case 307:
return "Temporary Redirect";
case 308:
return "Permanent Redirect";
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 402:
return "Payment Required";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 406:
return "Not Acceptable";
case 407:
return "Proxy Authentication Required";
case 408:
return "Request Timeout";
case 409:
return "Conflict";
case 410:
return "Gone";
case 411:
return "Length Required";
case 412:
return "Precondition Failed";
case 413:
return "Payload Too Large";
case 414:
return "URI Too Long";
case 415:
return "Unsupported Media Type";
case 416:
return "Range Not Satisfiable";
case 417:
return "Expectation Failed";
case 418:
return "I'm a teapot";
case 421:
return "Misdirected Request";
case 422:
return "Unprocessable Entity";
case 423:
return "Locked";
case 424:
return "Failed Dependency";
case 425:
return "Too Early";
case 426:
return "Upgrade Required";
case 428:
return "Precondition Required";
case 429:
return "Too Many Requests";
case 431:
return "Request Header Fields Too Large";
case 451:
return "Unavailable For Legal Reasons";
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Time-out";
case 505:
return "HTTP Version Not Supported";
case 506:
return "Variant Also Negotiates";
case 507:
return "Insufficient Storage";
case 508:
return "Loop Detected";
case 510:
return "Not Extended";
case 511:
return "Network Authentication Required";
}
return "<Unknown>";
}
std::string mimeTypeFromHeaders(
const std::map<std::string, std::string>& headers) {
std::string mimeType = "application/octet-stream";
for (const auto& [name, value] : headers) {
std::string lowerName = name;
std::transform(
lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
if (lowerName == "content-type") {
// Parse MIME type (discarding any parameters after ";") from the
// Content-Type header
// https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.1
size_t pos = value.find(';');
if (pos != std::string::npos) {
mimeType = value.substr(0, pos);
} else {
mimeType = value;
}
break;
}
}
return mimeType;
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,28 @@
/*
* 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 <map>
#include <string>
namespace facebook::react::jsinspector_modern {
using Headers = std::map<std::string, std::string>;
/**
* Get the HTTP reason phrase for a given status code (RFC 9110).
*/
std::string httpReasonPhrase(uint16_t status);
/**
* Get the MIME type for a response based on the 'Content-Type' header. If
* the header is not present, returns 'application/octet-stream'.
*/
std::string mimeTypeFromHeaders(const Headers &headers);
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,246 @@
/*
* 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 "NetworkHandler.h"
#include <jsinspector-modern/cdp/CdpJson.h>
#include <glog/logging.h>
#include <chrono>
namespace facebook::react::jsinspector_modern {
namespace {
/**
* Get the current Unix timestamp in seconds (µs precision, CDP format).
*/
double getCurrentUnixTimestampSeconds() {
auto now = std::chrono::system_clock::now().time_since_epoch();
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now).count();
auto micros =
std::chrono::duration_cast<std::chrono::microseconds>(now).count() %
1000000;
return static_cast<double>(seconds) +
(static_cast<double>(micros) / 1000000.0);
}
} // namespace
NetworkHandler& NetworkHandler::getInstance() {
static NetworkHandler instance;
return instance;
}
void NetworkHandler::setFrontendChannel(FrontendChannel frontendChannel) {
frontendChannel_ = std::move(frontendChannel);
}
bool NetworkHandler::enable() {
if (enabled_.load(std::memory_order_acquire)) {
return false;
}
enabled_.store(true, std::memory_order_release);
return true;
}
bool NetworkHandler::disable() {
if (!enabled_.load(std::memory_order_acquire)) {
return false;
}
enabled_.store(false, std::memory_order_release);
responseBodyBuffer_.clear();
return true;
}
void NetworkHandler::onRequestWillBeSent(
const std::string& requestId,
const cdp::network::Request& request,
const std::optional<cdp::network::Response>& redirectResponse) {
if (!isEnabledNoSync()) {
return;
}
double timestamp = getCurrentUnixTimestampSeconds();
std::optional<folly::dynamic> initiator;
initiator = consumeStoredRequestInitiator(requestId);
auto params = cdp::network::RequestWillBeSentParams{
.requestId = requestId,
.loaderId = "",
.documentURL = "mobile",
.request = request,
// NOTE: Both timestamp and wallTime use the same unit, however wallTime
// is relative to an "arbitrary epoch". In our implementation, use the
// Unix epoch for both.
.timestamp = timestamp,
.wallTime = timestamp,
.initiator = initiator.has_value()
? std::move(initiator.value())
: folly::dynamic::object("type", "script"),
.redirectHasExtraInfo = redirectResponse.has_value(),
.redirectResponse = redirectResponse,
};
frontendChannel_(
cdp::jsonNotification("Network.requestWillBeSent", params.toDynamic()));
}
void NetworkHandler::onRequestWillBeSentExtraInfo(
const std::string& requestId,
const Headers& headers) {
if (!isEnabledNoSync()) {
return;
}
auto params = cdp::network::RequestWillBeSentExtraInfoParams{
.requestId = requestId,
.headers = headers,
.connectTiming = {.requestTime = getCurrentUnixTimestampSeconds()},
};
frontendChannel_(
cdp::jsonNotification(
"Network.requestWillBeSentExtraInfo", params.toDynamic()));
}
void NetworkHandler::onResponseReceived(
const std::string& requestId,
const cdp::network::Response& response) {
if (!isEnabledNoSync()) {
return;
}
auto resourceType = cdp::network::resourceTypeFromMimeType(response.mimeType);
{
std::lock_guard<std::mutex> lock(requestMetadataMutex_);
resourceTypeMap_.emplace(requestId, resourceType);
}
auto params = cdp::network::ResponseReceivedParams{
.requestId = requestId,
.loaderId = "",
.timestamp = getCurrentUnixTimestampSeconds(),
.type = resourceType,
.response = response,
.hasExtraInfo = false,
};
frontendChannel_(
cdp::jsonNotification("Network.responseReceived", params.toDynamic()));
}
void NetworkHandler::onDataReceived(
const std::string& requestId,
int dataLength,
int encodedDataLength) {
if (!isEnabledNoSync()) {
return;
}
auto params = cdp::network::DataReceivedParams{
.requestId = requestId,
.timestamp = getCurrentUnixTimestampSeconds(),
.dataLength = dataLength,
.encodedDataLength = encodedDataLength,
};
frontendChannel_(
cdp::jsonNotification("Network.dataReceived", params.toDynamic()));
}
void NetworkHandler::onLoadingFinished(
const std::string& requestId,
int encodedDataLength) {
if (!isEnabledNoSync()) {
return;
}
auto params = cdp::network::LoadingFinishedParams{
.requestId = requestId,
.timestamp = getCurrentUnixTimestampSeconds(),
.encodedDataLength = encodedDataLength,
};
frontendChannel_(
cdp::jsonNotification("Network.loadingFinished", params.toDynamic()));
}
void NetworkHandler::onLoadingFailed(
const std::string& requestId,
bool cancelled) {
if (!isEnabledNoSync()) {
return;
}
{
std::lock_guard<std::mutex> lock(requestMetadataMutex_);
auto params = cdp::network::LoadingFailedParams{
.requestId = requestId,
.timestamp = getCurrentUnixTimestampSeconds(),
.type = resourceTypeMap_.find(requestId) != resourceTypeMap_.end()
? resourceTypeMap_.at(requestId)
: "Other",
.errorText = cancelled ? "net::ERR_ABORTED" : "net::ERR_FAILED",
.canceled = cancelled,
};
frontendChannel_(
cdp::jsonNotification("Network.loadingFailed", params.toDynamic()));
}
}
void NetworkHandler::storeResponseBody(
const std::string& requestId,
std::string_view body,
bool base64Encoded) {
std::lock_guard<std::mutex> lock(requestBodyMutex_);
responseBodyBuffer_.put(requestId, body, base64Encoded);
}
std::optional<std::tuple<std::string, bool>> NetworkHandler::getResponseBody(
const std::string& requestId) {
std::lock_guard<std::mutex> lock(requestBodyMutex_);
auto responseBody = responseBodyBuffer_.get(requestId);
if (responseBody == nullptr) {
return std::nullopt;
}
return std::make_optional<std::tuple<std::string, bool>>(
responseBody->data, responseBody->base64Encoded);
}
void NetworkHandler::recordRequestInitiatorStack(
const std::string& requestId,
folly::dynamic stackTrace) {
if (!isEnabledNoSync()) {
return;
}
std::lock_guard<std::mutex> lock(requestMetadataMutex_);
requestInitiatorById_.emplace(
requestId,
folly::dynamic::object("type", "script")("stack", std::move(stackTrace)));
}
std::optional<folly::dynamic> NetworkHandler::consumeStoredRequestInitiator(
const std::string& requestId) {
std::lock_guard<std::mutex> lock(requestMetadataMutex_);
auto it = requestInitiatorById_.find(requestId);
if (it == requestInitiatorById_.end()) {
return std::nullopt;
}
// Remove and return
auto result = std::move(it->second);
requestInitiatorById_.erase(it);
return result;
}
} // 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 "BoundedRequestBuffer.h"
#include "CdpNetwork.h"
#include <folly/dynamic.h>
#include <atomic>
#include <mutex>
#include <string>
#include <tuple>
namespace facebook::react::jsinspector_modern {
/**
* 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)>;
using Headers = std::map<std::string, std::string>;
/**
* [Experimental] Handler for reporting network events via CDP.
*/
class NetworkHandler {
public:
static NetworkHandler &getInstance();
/**
* Set the channel used to send CDP events to the frontend. This should be
* supplied before calling `enable`.
*/
void setFrontendChannel(FrontendChannel frontendChannel);
/**
* Enable network debugging. Returns `false` if already enabled.
*
* @cdp Network.enable
*/
bool enable();
/**
* Disable network debugging. Returns `false` if not initially enabled.
*
* @cdp Network.disable
*/
bool disable();
/**
* Returns whether network debugging is currently enabled.
*/
inline bool isEnabled() const
{
return enabled_.load(std::memory_order_acquire);
}
/**
* @cdp Network.requestWillBeSent
*/
void onRequestWillBeSent(
const std::string &requestId,
const cdp::network::Request &request,
const std::optional<cdp::network::Response> &redirectResponse);
/**
* @cdp Network.requestWillBeSentExtraInfo
*/
void onRequestWillBeSentExtraInfo(const std::string &requestId, const Headers &headers);
/**
* @cdp Network.responseReceived
*/
void onResponseReceived(const std::string &requestId, const cdp::network::Response &response);
/**
* @cdp Network.dataReceived
*/
void onDataReceived(const std::string &requestId, int dataLength, int encodedDataLength);
/**
* @cdp Network.loadingFinished
*/
void onLoadingFinished(const std::string &requestId, int encodedDataLength);
/**
* @cdp Network.loadingFailed
*/
void onLoadingFailed(const std::string &requestId, bool cancelled);
/**
* Store the fetched response body for a text or image network response.
*
* Reponse bodies are stored in a bounded buffer with a fixed maximum memory
* size, where oldest responses will be evicted if the buffer is exceeded.
*
* Should be called after checking \ref NetworkHandler::isEnabled.
*/
void storeResponseBody(const std::string &requestId, std::string_view body, bool base64Encoded);
/**
* Retrieve a stored response body for a given request ID.
*
* \returns An optional tuple of [responseBody, base64Encoded]. Returns
* nullopt if no entry is found in the buffer.
*/
std::optional<std::tuple<std::string, bool>> getResponseBody(const std::string &requestId);
/**
* Associate the given stack trace with the given request ID.
*/
void recordRequestInitiatorStack(const std::string &requestId, folly::dynamic stackTrace);
private:
NetworkHandler() = default;
NetworkHandler(const NetworkHandler &) = delete;
NetworkHandler &operator=(const NetworkHandler &) = delete;
~NetworkHandler() = default;
std::atomic<bool> enabled_{false};
inline bool isEnabledNoSync() const
{
return enabled_.load(std::memory_order_relaxed);
}
std::optional<folly::dynamic> consumeStoredRequestInitiator(const std::string &requestId);
FrontendChannel frontendChannel_;
std::map<std::string, std::string> resourceTypeMap_{};
std::map<std::string, folly::dynamic> requestInitiatorById_{};
std::mutex requestMetadataMutex_{};
BoundedRequestBuffer responseBodyBuffer_{};
std::mutex requestBodyMutex_;
};
} // 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.
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
header_search_paths = []
if ENV['USE_FRAMEWORKS']
header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../..\""
end
header_dir = 'jsinspector-modern/network'
module_name = "jsinspector_modernnetwork"
Pod::Spec.new do |s|
s.name = "React-jsinspectornetwork"
s.version = version
s.summary = "Network inspection for React Native DevTools"
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 = podspec_sources("*.{cpp,h}", "*.h")
s.header_dir = header_dir
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"DEFINES_MODULE" => "YES"}
resolve_use_frameworks(s, header_mappings_dir: "../..", module_name: module_name)
add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp')
add_rn_third_party_dependencies(s)
add_rncore_dependency(s)
end

View File

@@ -0,0 +1,916 @@
/*
* 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 "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
#include "prelude.js.h"
#include <utility>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
namespace {
struct Params {
/**
* Whether to evaluate the prelude.js script (containing RN's console
* polyfill) after setting up the Runtime.
*/
bool withConsolePolyfill{false};
/**
* Whether to install the global nativeLoggingHook function after setting up
* the Runtime (before the prelude if any).
*/
bool withNativeLoggingHook{false};
/**
* Whether to enable the Runtime domain at the start of the test (and expect
* live consoleAPICalled notifications), or enable it at the *end* of the test
* (and expect buffered notifications at that point).
*/
bool runtimeEnabledAtStart{false};
};
} // namespace
/**
* A test fixture for the Console API.
*/
class ConsoleApiTest : public JsiIntegrationPortableTestBase<
JsiIntegrationTestHermesEngineAdapter,
folly::QueuedImmediateExecutor>,
public WithParamInterface<Params> {
protected:
void SetUp() override {
JsiIntegrationPortableTestBase::SetUp();
connect();
EXPECT_CALL(
fromPage(),
onMessage(
JsonParsed(AllOf(AtJsonPtr("/method", "Debugger.scriptParsed")))))
.Times(AnyNumber())
.WillRepeatedly(Invoke<>([this](const std::string& message) {
auto params = folly::parseJson(message);
// Store the script ID and URL for later use.
scriptUrlsById_.emplace(
params.at("params").at("scriptId").getString(),
params.at("params").at("url").getString());
}));
this->expectMessageFromPage(JsonEq(R"({
"id": 0,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 0,
"method": "Debugger.enable"
})");
if (GetParam().runtimeEnabledAtStart) {
enableRuntimeDomain();
}
}
void TearDown() override {
if (!GetParam().runtimeEnabledAtStart) {
enableRuntimeDomain();
}
JsiIntegrationPortableTestBase::TearDown();
}
/**
* Expect a console API call to be reported with parameters matching \param
* paramsMatcher.
*/
void expectConsoleApiCall(
Matcher<folly::dynamic> paramsMatcher,
std::source_location location = std::source_location::current()) {
if (runtimeEnabled_) {
expectConsoleApiCallImpl(std::move(paramsMatcher), location);
} else {
expectedConsoleApiCalls_.emplace_back(paramsMatcher, location);
}
}
/**
* Expect a console API call to be reported with parameters matching \param
* paramsMatcher, only if the Runtime domain is currently enabled ( = the call
* is reported in real time).
*/
void expectConsoleApiCallImmediate(
Matcher<folly::dynamic> paramsMatcher,
std::source_location location = std::source_location::current()) {
if (runtimeEnabled_) {
expectConsoleApiCallImpl(std::move(paramsMatcher), location);
}
}
/**
* Expect a console API call to be reported with parameters matching \param
* paramsMatcher, only if the Runtime domain is currently disabled ( = the
* call will be buffered and reported later upon enabling the domain).
*/
void expectConsoleApiCallBuffered(
const Matcher<folly::dynamic>& paramsMatcher,
std::source_location location = std::source_location::current()) {
if (!runtimeEnabled_) {
expectedConsoleApiCalls_.emplace_back(paramsMatcher, location);
}
}
bool isRuntimeDomainEnabled() const {
return runtimeEnabled_;
}
void clearExpectedConsoleApiCalls() {
expectedConsoleApiCalls_.clear();
}
template <typename InnerMatcher>
Matcher<folly::dynamic> ScriptIdMapsTo(InnerMatcher urlMatcher) {
return ResultOf(
[this](const auto& id) { return getScriptUrlById(id.getString()); },
urlMatcher);
}
private:
std::optional<std::string> getScriptUrlById(const std::string& scriptId) {
auto it = scriptUrlsById_.find(scriptId);
if (it == scriptUrlsById_.end()) {
return std::nullopt;
}
return it->second;
}
void expectConsoleApiCallImpl(
Matcher<folly::dynamic> paramsMatcher,
std::source_location location) {
this->expectMessageFromPage(
JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"),
AtJsonPtr("/params", std::move(paramsMatcher)))),
location);
}
void enableRuntimeDomain() {
InSequence s;
auto executionContextInfo = this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
if (!runtimeEnabled_) {
for (auto& [call, location] : expectedConsoleApiCalls_) {
expectConsoleApiCallImpl(call, location);
}
expectedConsoleApiCalls_.clear();
}
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
ASSERT_TRUE(executionContextInfo->has_value());
runtimeEnabled_ = true;
}
void loadMainBundle() override {
auto params = GetParam();
if (params.withNativeLoggingHook) {
// The presence or absence of nativeLoggingHook affects the console
// polyfill's behaviour.
eval(
R"(
if (!globalThis.nativeLoggingHook) {
globalThis.nativeLoggingHook = function(level, message) {
print(level + ': ' + message);
};
}
)");
} else {
// Ensure that we run without nativeLoggingHook even if it was installed
// elsewhere.
eval(R"(
delete globalThis.nativeLoggingHook;
)");
}
if (params.withConsolePolyfill) {
eval(preludeJsCode);
}
}
std::vector<std::pair<Matcher<folly::dynamic>, std::source_location>>
expectedConsoleApiCalls_;
bool runtimeEnabled_{false};
std::unordered_map<std::string, std::string> scriptUrlsById_;
};
class ConsoleApiTestWithPreExistingConsole : public ConsoleApiTest {
void setupRuntimeBeforeRegistration(jsi::Runtime& /*unused*/) override {
eval(R"(
globalThis.__console_messages__ = [];
globalThis.console = {
log: function(...args) {
globalThis.__console_messages__.push({
type: 'log',
args,
});
},
warn: function(...args) {
globalThis.__console_messages__.push({
type: 'warn',
args,
});
},
};
)");
}
};
TEST_P(ConsoleApiTest, testConsoleLog) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello"
}, {
"type": "string",
"value": "world"
}])"_json)));
eval("console.log('hello', 'world');");
}
TEST_P(ConsoleApiTest, testConsoleDebug) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "debug"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello fusebox"
}])"_json)));
eval("console.debug('hello fusebox');");
}
TEST_P(ConsoleApiTest, testConsoleInfo) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "info"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "you should know this"
}])"_json)));
eval("console.info('you should know this');");
}
TEST_P(ConsoleApiTest, testConsoleError) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "error"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "uh oh"
}])"_json)));
eval("console.error('uh oh');");
}
TEST_P(ConsoleApiTest, testConsoleLogWithErrorObject) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr("/args/0/type", "object"),
AtJsonPtr("/args/0/subtype", "error"),
AtJsonPtr("/args/0/className", "Error"),
AtJsonPtr(
"/args/0/description",
"Error: wut\n"
" at secondFunction (<eval>:6:28)\n"
" at firstFunction (<eval>:3:21)\n"
" at anonymous (<eval>:8:18)\n"
" at global (<eval>:9:5)")));
eval(R"((() => {
function firstFunction() {
secondFunction();
}
function secondFunction() {
console.log(new Error('wut'));
}
firstFunction();
})())");
}
TEST_P(ConsoleApiTest, testConsoleLogWithArrayOfErrors) {
InSequence s;
expectConsoleApiCallImmediate(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr("/args/0/type", "object"),
AtJsonPtr("/args/0/subtype", "array"),
AtJsonPtr("/args/0/description", "Array(2)"),
AtJsonPtr("/args/0/preview/description", "Array(2)"),
AtJsonPtr("/args/0/preview/type", "object"),
AtJsonPtr("/args/0/preview/subtype", "array"),
AtJsonPtr("/args/0/preview/properties/0/type", "object"),
AtJsonPtr("/args/0/preview/properties/0/subtype", "error"),
AtJsonPtr(
"/args/0/preview/properties/0/value",
"Error: wut\n"
" at typicallyUrlsAreLongAndWillHitTheAbbreviationLimit (<eval>:6:29)\n"
" at reallyLon…")));
expectConsoleApiCallBuffered(AllOf(AtJsonPtr("/type", "log")));
eval(R"((() => {
function reallyLongFunctionNameToAssertMaxLengthOfAbbreviatedString() {
typicallyUrlsAreLongAndWillHitTheAbbreviationLimit();
}
function typicallyUrlsAreLongAndWillHitTheAbbreviationLimit() {
console.log([new Error('wut'), new TypeError('why')]);
}
reallyLongFunctionNameToAssertMaxLengthOfAbbreviatedString();
})())");
}
TEST_P(ConsoleApiTest, testConsoleWarn) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "careful"
}])"_json)));
eval("console.warn('careful');");
}
TEST_P(ConsoleApiTest, testConsoleDir) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "dir"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "something"
}])"_json)));
eval("console.dir('something');");
}
TEST_P(ConsoleApiTest, testConsoleDirxml) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "dirxml"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "pretend this is a DOM element"
}])"_json)));
eval("console.dirxml('pretend this is a DOM element');");
}
TEST_P(ConsoleApiTest, testConsoleTable) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "table"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "pretend this is a complex object"
}])"_json)));
eval("console.table('pretend this is a complex object');");
}
TEST_P(ConsoleApiTest, testConsoleTrace) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "trace"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "trace trace"
}])"_json)));
eval("console.trace('trace trace');");
}
TEST_P(ConsoleApiTest, testConsoleClear) {
InSequence s;
expectConsoleApiCall(
AllOf(AtJsonPtr("/type", "clear"), AtJsonPtr("/args", "[]"_json)));
eval("console.clear();");
}
TEST_P(ConsoleApiTest, testConsoleClearAfterOtherCall) {
InSequence s;
if (isRuntimeDomainEnabled()) {
// This should only be delivered if console notifications are enabled, not
// when they're being cached for later.
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello"
}])"_json)));
}
expectConsoleApiCall(
AllOf(AtJsonPtr("/type", "clear"), AtJsonPtr("/args", "[]"_json)));
eval("console.log('hello');");
eval("console.clear();");
}
TEST_P(ConsoleApiTest, testConsoleGroup) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "startGroup"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "group title"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "in group"
}])"_json)));
expectConsoleApiCall(
AllOf(AtJsonPtr("/type", "endGroup"), AtJsonPtr("/args", "[]"_json)));
eval("console.group('group title');");
eval("console.log('in group');");
eval("console.groupEnd();");
}
TEST_P(ConsoleApiTest, testConsoleGroupCollapsed) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "startGroupCollapsed"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "group collapsed title"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "in group collapsed"
}])"_json)));
expectConsoleApiCall(
AllOf(AtJsonPtr("/type", "endGroup"), AtJsonPtr("/args", "[]"_json)));
eval("console.groupCollapsed('group collapsed title');");
eval("console.log('in group collapsed');");
eval("console.groupEnd();");
}
TEST_P(ConsoleApiTest, testConsoleAssert) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "assert"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Assertion failed: something is bad"
}])"_json)));
eval("console.assert(true, 'everything is good');");
eval("console.assert(false, 'something is bad');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "assert"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Assertion failed"
}])"_json)));
eval("console.assert();");
}
TEST_P(ConsoleApiTest, testConsoleCount) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "default: 1"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "default: 2"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "default: 3"
}])"_json)));
eval("console.count();");
eval("console.count('default');");
eval("console.count();");
eval("console.countReset();");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "default: 1"
}])"_json)));
eval("console.count();");
eval("console.countReset('default');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "default: 1"
}])"_json)));
eval("console.count();");
}
TEST_P(ConsoleApiTest, testConsoleCountLabel) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "foo: 1"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "foo: 2"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "foo: 3"
}])"_json)));
eval("console.count('foo');");
eval("console.count('foo');");
eval("console.count('foo');");
eval("console.countReset('foo');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "count"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "foo: 1"
}])"_json)));
eval("console.count('foo');");
}
TEST_P(ConsoleApiTest, testConsoleCountResetInvalidLabel) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Count for 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Count for 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Count for 'foo' does not exist"
}])"_json)));
eval("console.countReset();");
eval("console.countReset('default');");
eval("console.countReset('foo');");
}
// TODO(moti): Tests for console.timeEnd() and timeLog() that actually check the
// output (with mocked system clock?)
TEST_P(ConsoleApiTest, testConsoleTimeExistingLabel) {
eval("console.time();");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'default' already exists"
}])"_json)));
eval("console.time('default');");
eval("console.time('foo');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'foo' already exists"
}])"_json)));
eval("console.time('foo');");
}
TEST_P(ConsoleApiTest, testConsoleTimeInvalidLabel) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'foo' does not exist"
}])"_json)));
eval("console.timeEnd();");
eval("console.timeEnd('default');");
eval("console.timeEnd('foo');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'default' does not exist"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "Timer 'foo' does not exist"
}])"_json)));
eval("console.timeLog();");
eval("console.timeLog('default');");
eval("console.timeLog('foo');");
}
TEST_P(ConsoleApiTest, testConsoleSilentlyClearedOnReload) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello"
}])"_json)));
eval("console.log('hello');");
// If there are any expectations we haven't checked yet, clear them
clearExpectedConsoleApiCalls();
// Reloading generates some Runtime events
if (isRuntimeDomainEnabled()) {
expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextDestroyed"))));
expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextsCleared"))));
expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
}
reload();
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "world"
}])"_json)));
eval("console.log('world');");
}
TEST_P(ConsoleApiTestWithPreExistingConsole, testPreExistingConsoleObject) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "warning"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "world"
}])"_json)));
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "table"),
AtJsonPtr(
"/args",
R"([{
"type": "number",
"value": 42
}])"_json)));
eval("console.log('hello');");
eval("console.warn('world');");
// NOTE: not present in the pre-existing console object
eval("console.table(42);");
auto& runtime = engineAdapter_->getRuntime();
EXPECT_THAT(
eval("JSON.stringify(globalThis.__console_messages__)")
.asString(runtime)
.utf8(runtime),
JsonEq(
R"([{
"type": "log",
"args": [
"hello"
]
}, {
"type": "warn",
"args": [
"world"
]
}])"));
}
TEST_P(ConsoleApiTest, testConsoleLogStack) {
InSequence s;
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr(
"/args",
R"([{
"type": "string",
"value": "hello"
}])"_json),
AtJsonPtr(
"/stackTrace/callFrames",
AllOf(
Each(AtJsonPtr(
"/url",
Conditional(
GetParam().withConsolePolyfill,
AnyOf("script.js", "prelude.js"),
"script.js"))),
// A relatively weak assertion: we expect at least one frame tying
// the call to the `console.log` line.
Contains(AllOf(
AtJsonPtr("/functionName", "global"),
AtJsonPtr("/url", "script.js"),
AtJsonPtr("/lineNumber", 1),
AtJsonPtr("/scriptId", ScriptIdMapsTo("script.js"))))))));
eval(R"( // line 0
console.log('hello'); // line 1
//# sourceURL=script.js
)");
}
TEST_P(ConsoleApiTest, testConsoleLogTwice) {
InSequence s;
expectConsoleApiCall(
AllOf(AtJsonPtr("/type", "log"), AtJsonPtr("/args/0/value", "hello")));
eval("console.log('hello');");
expectConsoleApiCall(AllOf(
AtJsonPtr("/type", "log"), AtJsonPtr("/args/0/value", "hello again")));
eval("console.log('hello again');");
}
TEST_P(ConsoleApiTest, testConsoleLogWithObjectPreview) {
InSequence s;
expectConsoleApiCallImmediate(AllOf(
AtJsonPtr("/type", "log"),
AtJsonPtr("/args/0/preview/type", "object"),
AtJsonPtr("/args/0/preview/overflow", false),
AtJsonPtr("/args/0/preview/properties/0/name", "string"),
AtJsonPtr("/args/0/preview/properties/0/type", "string"),
AtJsonPtr("/args/0/preview/properties/0/value", "hello")));
expectConsoleApiCallBuffered(AllOf(AtJsonPtr("/type", "log")));
eval("console.log({ string: 'hello' });");
}
static const auto paramValues = testing::Values(
Params{
.withConsolePolyfill = true,
.withNativeLoggingHook = false,
.runtimeEnabledAtStart = false,
},
Params{
.withConsolePolyfill = false,
.withNativeLoggingHook = false,
.runtimeEnabledAtStart = false,
},
Params{
.withConsolePolyfill = true,
.withNativeLoggingHook = false,
.runtimeEnabledAtStart = true,
},
Params{
.withConsolePolyfill = false,
.withNativeLoggingHook = false,
.runtimeEnabledAtStart = true,
},
Params{
.withConsolePolyfill = true,
.withNativeLoggingHook = true,
.runtimeEnabledAtStart = false,
},
Params{
.withConsolePolyfill = false,
.withNativeLoggingHook = true,
.runtimeEnabledAtStart = false,
},
Params{
.withConsolePolyfill = true,
.withNativeLoggingHook = true,
.runtimeEnabledAtStart = true,
},
Params{
.withConsolePolyfill = false,
.withNativeLoggingHook = true,
.runtimeEnabledAtStart = true,
});
INSTANTIATE_TEST_SUITE_P(ConsoleApiTest, ConsoleApiTest, paramValues);
INSTANTIATE_TEST_SUITE_P(
ConsoleApiTestWithPreExistingConsole,
ConsoleApiTestWithPreExistingConsole,
paramValues);
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,143 @@
/*
* 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 "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
#include <jsinspector-modern/tracing/PerformanceTracer.h>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
/**
* A test fixture for the console.timeStamp API.
*/
class ConsoleTimeStampTest : public JsiIntegrationPortableTestBase<
JsiIntegrationTestHermesEngineAdapter,
folly::QueuedImmediateExecutor> {
protected:
size_t countNumberOfTimeStampEvents(
const std::vector<tracing::TraceEvent>& events) {
size_t count = 0;
for (const auto& event : events) {
if (event.name == "TimeStamp") {
count++;
}
}
return count;
}
};
TEST_F(ConsoleTimeStampTest, Installed) {
auto result = eval("typeof console.timeStamp");
auto& runtime = engineAdapter_->getRuntime();
EXPECT_EQ(result.asString(runtime).utf8(runtime), "function");
}
TEST_F(ConsoleTimeStampTest, RecordsEntriesWithJustLabel) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('test-label')");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, RecordsEntriesWithSpecifiedTimestamps) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('test-range', 100, 200)");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, RecordsEntriesWithMarkNames) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval(
"console.timeStamp('test-string-timestamps', 'start-marker', 'end-marker')");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, KeepsEntriesWithUnknownMarkNames) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval(
"console.timeStamp('test-string-timestamps', 'start-marker', 'end-marker')");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, DoesNotThrowIfNotTracing) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_FALSE(tracer.stopTracing());
// Call console.timeStamp - should be a no-op when not tracing
eval("console.timeStamp('test-no-tracing')");
}
TEST_F(ConsoleTimeStampTest, SurvivesInvalidArguments) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
// Won't be logged, no label specified.
eval("console.timeStamp()");
// Will be logged - undefined will be stringified.
eval("console.timeStamp(undefined)");
// Will be logged - function will be stringified.
eval("console.timeStamp(() => {})");
// Will be logged - 123 will be stringified.
eval("console.timeStamp(123)");
// Will be logged - start will default to an empty string.
eval("console.timeStamp('label', {})");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 4);
}
TEST_F(ConsoleTimeStampTest, InvalidTrackNameIsIgnored) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('label', 0, 1, {})");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, InvalidTrackGroupIsIgnored) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('label', 0, 1, 'trackName', {})");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
TEST_F(ConsoleTimeStampTest, InvalidColorIsIgnored) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('test', 100, 200, 'FooTrack', 'BarGroup', 'red')");
auto events = tracer.stopTracing();
EXPECT_EQ(countNumberOfTimeStampEvents(*events), 1);
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,195 @@
/*
* 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 "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
class DebuggerSessionObserverTest : public JsiIntegrationPortableTestBase<
JsiIntegrationTestHermesEngineAdapter,
folly::QueuedImmediateExecutor> {
protected:
void enableRuntimeDomain() {
InSequence s;
auto executionContextInfo = expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
}
void enableLogDomain() {
InSequence s;
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Log.entryAdded"),
AtJsonPtr("/params/entry", Not(IsEmpty()))))))
.Times(AtLeast(1));
expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
toPage_->sendMessage(R"({
"id": 1,
"method": "Log.enable"
})");
}
void disableRuntimeDomain() {
InSequence s;
expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.disable"
})");
}
void disableLogDomain() {
InSequence s;
expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
toPage_->sendMessage(R"({
"id": 1,
"method": "Log.disable"
})");
}
};
TEST_F(DebuggerSessionObserverTest, InstallsGlobalObserverObjectByDefault) {
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__ != null").asBool());
}
TEST_F(
DebuggerSessionObserverTest,
WillNotEmitStatusUpdateUnlessBothRuntimeAndLogDomainsAreEnabled) {
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
connect();
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
enableRuntimeDomain();
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
enableLogDomain();
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
}
TEST_F(
DebuggerSessionObserverTest,
UpdatesTheStatusOnceRuntimeDomainIsDisabled) {
connect();
enableLogDomain();
enableRuntimeDomain();
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
disableRuntimeDomain();
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
}
TEST_F(DebuggerSessionObserverTest, UpdatesTheStatusOnceLogDomainIsDisabled) {
connect();
enableLogDomain();
enableRuntimeDomain();
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
disableLogDomain();
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
}
TEST_F(
DebuggerSessionObserverTest,
NotifiesSubscribersWhichWereSubscribedBeforeSessionInitialization) {
eval(
R"(
var latestStatus = undefined;
__DEBUGGER_SESSION_OBSERVER__.subscribers.add(updatedStatus => {
latestStatus = updatedStatus;
});
)");
EXPECT_TRUE(eval("latestStatus").isUndefined());
connect();
enableLogDomain();
enableRuntimeDomain();
EXPECT_TRUE(eval("latestStatus").asBool());
disableLogDomain();
EXPECT_FALSE(eval("latestStatus").asBool());
}
TEST_F(DebuggerSessionObserverTest, testTwoConcurrentConnections) {
connect();
auto secondary = connectSecondary();
EXPECT_CALL(fromPage(), onMessage(_)).Times(AnyNumber());
EXPECT_CALL(secondary.fromPage(), onMessage(_)).Times(AnyNumber());
// No domains enabled to start with
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
toPage_->sendMessage(R"({"id": 1, "method": "Runtime.enable"})");
// Primary: Runtime, Secondary: None
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
secondary.toPage().sendMessage(R"({"id": 2, "method": "Log.enable"})");
// Primary: Runtime, Secondary: Log
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
secondary.toPage().sendMessage(R"({"id": 3, "method": "Runtime.enable"})");
// Primary: Runtime, Secondary: [Runtime, Log]
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
toPage_->sendMessage(R"({"id": 4, "method": "Log.enable"})");
// Primary: [Runtime, Log], Secondary: [Runtime, Log]
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
toPage_->sendMessage(R"({"id": 5, "method": "Runtime.disable"})");
// Primary: Log, Secondary: [Runtime, Log]
EXPECT_TRUE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
secondary.toPage().sendMessage(R"({"id": 6, "method": "Log.disable"})");
// Primary: Log, Secondary: Runtime
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
secondary.toPage().sendMessage(R"({"id": 7, "method": "Runtime.disable"})");
// Primary: Log, Secondary: None
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
toPage_->sendMessage(R"({"id": 8, "method": "Log.disable"})");
// Primary: None, Secondary: None
EXPECT_FALSE(eval("__DEBUGGER_SESSION_OBSERVER__.hasActiveSession").asBool());
}
} // 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(const folly::dynamic& value) {
return value.asString();
}
std::string explain_error(
folly::dynamic::json_pointer_resolution_error<const folly::dynamic> 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,101 @@
/*
* 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(const folly::dynamic &value);
std::string explain_error(folly::dynamic::json_pointer_resolution_error<const folly::dynamic> 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;
}
/**
* A higher-order matcher that applies an inner matcher to the string value of
* a folly::dynamic.
*/
MATCHER_P(
DynamicString,
innerMatcher,
std::string{"string value "} + testing::DescribeMatcher<std::string>(innerMatcher, negation))
{
using namespace ::testing;
using namespace folly_dynamic_matchers_utils;
if (!arg.isString()) {
*result_listener << "is not a string";
return false;
}
return ExplainMatchResult(innerMatcher, arg.getString(), result_listener);
}
/**
* A user-defined literal for constructing a folly::dynamic from a JSON
* string. Not technically specific to GMock, but convenient to have in a test
* suite.
*/
inline folly::dynamic operator""_json(const char *s, size_t n)
{
return folly::parseJson(std::string{s, n});
}
} // namespace facebook

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.
*/
#include <gmock/gmock.h>
#pragma once
/**
* A variant of GMOCK_ON_CALL_IMPL that allows specifying the source location as
* a std::source_location parameter.
*/
#define GMOCK_ON_CALL_WITH_SOURCE_LOCATION_IMPL_(location, mock_expr, Setter, call) \
((mock_expr).gmock_##call)(::testing::internal::GetWithoutMatchers(), nullptr) \
.Setter((location).file_name(), (location).line(), #mock_expr, #call)
/**
* A variant of EXPECT_CALL that allows specifying the source location as a
* std::source_location parameter;
*/
#define EXPECT_CALL_WITH_SOURCE_LOCATION(location, obj, call) \
GMOCK_ON_CALL_WITH_SOURCE_LOCATION_IMPL_(location, obj, InternalExpectedAt, call)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,195 @@
/*
* 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);
}
}));
EXPECT_CALL(*this, scheduleCallback(_, _)).Times(AnyNumber());
}
// 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 MockHostTargetDelegate : public HostTargetDelegate {
public:
// HostTargetDelegate methods
HostTargetMetadata getMetadata() override
{
return {.integrationName = "MockHostTargetDelegate"};
}
MOCK_METHOD(void, onReload, (const PageReloadRequest &request), (override));
MOCK_METHOD(
void,
onSetPausedInDebuggerMessage,
(const OverlaySetPausedInDebuggerMessageRequest &request),
(override));
MOCK_METHOD(
void,
loadNetworkResource,
(const LoadNetworkResourceRequest &params, ScopedExecutor<NetworkRequestListener> executor),
(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 &,
RuntimeExecutor),
(override));
MOCK_METHOD(void, addConsoleMessage, (jsi::Runtime & runtime, ConsoleMessage message), (override));
MOCK_METHOD(bool, supportsConsole, (), (override, const));
MOCK_METHOD(
std::unique_ptr<StackTrace>,
captureStackTrace,
(jsi::Runtime & runtime, size_t framesToSkip),
(override));
MOCK_METHOD(void, enableSamplingProfiler, (), (override));
MOCK_METHOD(void, disableSamplingProfiler, (), (override));
MOCK_METHOD(tracing::RuntimeSamplingProfile, collectSamplingProfile, (), (override));
MOCK_METHOD(std::optional<folly::dynamic>, serializeStackTrace, (const StackTrace &stackTrace), (override));
inline MockRuntimeTargetDelegate()
{
using namespace testing;
// Silence "uninteresting mock function call" warnings for methods that
// don't have side effects.
EXPECT_CALL(*this, supportsConsole()).Times(AnyNumber());
}
};
class MockRuntimeAgentDelegate : public RuntimeAgentDelegate {
public:
inline MockRuntimeAgentDelegate(
FrontendChannel frontendChannel,
SessionState &sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState> /*unused*/,
ExecutionContextDescription executionContextDescription,
const RuntimeExecutor & /*runtimeExecutor*/)
: frontendChannel(std::move(frontendChannel)),
sessionState(sessionState),
executionContextDescription(std::move(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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,988 @@
/*
* 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 <fmt/format.h>
#include <folly/executors/ManualExecutor.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestGenericEngineAdapter.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
////////////////////////////////////////////////////////////////////////////////
// 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>;
template <typename EngineAdapter>
using JsiIntegrationPortableTest = JsiIntegrationPortableTestBase<
EngineAdapter,
folly::QueuedImmediateExecutor>;
TYPED_TEST_SUITE(JsiIntegrationPortableTest, AllEngines);
template <typename EngineAdapter>
using JsiIntegrationHermesTest = JsiIntegrationPortableTestBase<
EngineAdapter,
folly::QueuedImmediateExecutor>;
/**
* Fixture class for tests that run on a ManualExecutor. Work scheduled
* on the executor is *not* run automatically; it must be manually advanced
* in the body of the test.
*/
template <typename EngineAdapter>
class JsiIntegrationHermesTestAsync : public JsiIntegrationPortableTestBase<
EngineAdapter,
folly::ManualExecutor> {
public:
void TearDown() override {
// Assert there are no pending tasks on the ManualExecutor.
auto tasksCleared = this->executor_.clear();
EXPECT_EQ(tasksCleared, 0)
<< "There were still pending tasks on executor_ at the end of the test. Use advance() or run() as needed.";
JsiIntegrationPortableTestBase<EngineAdapter, folly::ManualExecutor>::
TearDown();
}
};
TYPED_TEST_SUITE(JsiIntegrationHermesTest, AllHermesVariants);
TYPED_TEST_SUITE(JsiIntegrationHermesTestAsync, AllHermesVariants);
#pragma region AllEngines
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"
})");
ASSERT_TRUE(executionContextInfo->has_value());
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"
})");
ASSERT_TRUE(executionContextInfo->has_value());
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(JsiIntegrationPortableTest, ReactNativeApplicationEnable) {
this->connect();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "ReactNativeApplication.metadataUpdated",
"params": {
"integrationName": "JsiIntegrationTest",
"unstable_isProfilingBuild": false,
"unstable_networkInspectionEnabled": false
}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "ReactNativeApplication.enable",
"params": {}
})");
}
TYPED_TEST(JsiIntegrationPortableTest, ReactNativeApplicationDisable) {
this->connect();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "ReactNativeApplication.disable",
"params": {}
})");
}
#pragma region JsiIntegrationPortableTestWithEarlyAddedBinding
// A test with some special setup to cover an edge case in the CDP backend:
// bindings can be added before there's even a Runtime in the session, and
// they should still work seamlessly once there is one.
TYPED_TEST_SUITE(JsiIntegrationPortableTestWithEarlyAddedBinding, AllEngines);
template <typename EngineAdapter>
class JsiIntegrationPortableTestWithEarlyAddedBinding
: public JsiIntegrationPortableTest<EngineAdapter> {
void setupRuntimeBeforeRegistration(jsi::Runtime& /*unused*/) override {
// Connect early, before the RuntimeTarget is created. This session will not
// have a RuntimeAgent yet.
this->connect();
// Add a binding.
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.addBinding",
"params": {"name": "foo"}
})");
// The binding is not installed yet, because the runtime is not registered
// with the CDP backend yet.
EXPECT_TRUE(this->eval("typeof globalThis.foo === 'undefined'").getBool());
}
};
TYPED_TEST(
JsiIntegrationPortableTestWithEarlyAddedBinding,
AddBindingBeforeRuntimeRegistered) {
// Now there is a RuntimeTarget / RuntimeAgent in the session, as per the
// normal test setup. Note that we're already connect()ed - not repeating that
// here.
// The binding is now installed, callable and working.
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.bindingCalled"),
AtJsonPtr("/params/name", "foo"),
AtJsonPtr("/params/payload", "bar"))));
this->eval("globalThis.foo('bar');");
}
#pragma endregion // JsiIntegrationPortableTestWithEarlyAddedBinding
#pragma endregion // AllEngines
#pragma region AllHermesVariants
TYPED_TEST(JsiIntegrationHermesTestAsync, HermesObjectsTableDoesNotMemoryLeak) {
// This is a regression test for T186157855 (CDPAgent leaking JSI data in
// RemoteObjectsTable past the Runtime's lifetime)
this->connect();
this->executor_.run();
InSequence s;
this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
this->executor_.run();
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"),
AtJsonPtr("/params/args/0/objectId", "1"))));
this->eval(R"(console.log({a: 1});)");
this->executor_.run();
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"
}
}
})"));
// NOTE: Doesn't crash when Hermes checks for JSI value leaks
this->reload();
this->executor_.run();
}
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"
})");
ASSERT_TRUE(executionContextInfo->has_value());
auto executionContextId =
executionContextInfo->value()["params"]["context"]["id"].getInt();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {
"result": {
"type": "number",
"value": 42
}
}
})"));
this->toPage_->sendMessage(
fmt::format(
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", -32600))));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 3,
"method": "Runtime.evaluate",
"params": {{"expression": "10000", "contextId": {0}}}
}})",
std::to_string(executionContextId)));
}
#if !defined(HERMES_STATIC_HERMES)
// FIXME(T239924718): Breakpoint resolution in Static Hermes is broken for
// locations without column numbers under lazy compilation.
TYPED_TEST(JsiIntegrationHermesTest, ResolveBreakpointAfterEval) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Debugger.enable"
})");
auto scriptInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Debugger.scriptParsed"),
AtJsonPtr("/params/url", "breakpointTest.js"))));
this->eval(R"( // line 0
globalThis.foo = function() { // line 1
Date.now(); // line 2
};
//# sourceURL=breakpointTest.js
)");
ASSERT_TRUE(scriptInfo->has_value());
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/id", 2),
AtJsonPtr("/result/locations/0/lineNumber", 2),
AtJsonPtr(
"/result/locations/0/scriptId",
scriptInfo->value()["params"]["scriptId"]))));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Debugger.setBreakpointByUrl",
"params": {"lineNumber": 2, "url": "breakpointTest.js"}
})");
}
TYPED_TEST(JsiIntegrationHermesTest, ResolveBreakpointAfterReload) {
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Debugger.enable"
})");
this->expectMessageFromPage(JsonParsed(AtJsonPtr("/id", 2)));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Debugger.setBreakpointByUrl",
"params": {"lineNumber": 2, "url": "breakpointTest.js"}
})");
this->reload();
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
)");
ASSERT_TRUE(breakpointInfo->has_value());
ASSERT_TRUE(scriptInfo->has_value());
EXPECT_EQ(
breakpointInfo->value()["params"]["location"]["scriptId"],
scriptInfo->value()["params"]["scriptId"]);
}
#endif // !defined(HERMES_STATIC_HERMES)
TYPED_TEST(JsiIntegrationHermesTest, CDPAgentReentrancyRegressionTest) {
this->connect();
InSequence s;
this->inspectorExecutor_([&]() {
// Tasks scheduled on our executor here will be executed when this lambda
// returns. This is integral to the bug we're trying to reproduce, so we
// place the EXPECT_* calls at the end of the lambda body to ensure the
// test fails if we get eager (unexpected) responses.
// 1. Cause CDPAgent to schedule a task to process the message. Originally,
// the task would be simultaneously scheduled on the JS executor, and as
// an interrupt on the JS interpreter. It's called via the executor
// regardless, since the interpreter is idle at the moment.
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": "Math.random(); /* Interrupts processed here. */ globalThis.x = 1 + 2"}
})");
// 2. Cause CDPAgent to schedule another task. If scheduled as an interrupt,
// this task will run _during_ the first task.
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Runtime.evaluate",
"params": {"expression": "globalThis.x = 3 + 4"}
})");
// This setup used to trigger three distinct bugs in CDPAgent:
// - The first task would be triggered twice due to a race condition
// between the executor and the interrupt handler. (D54771697)
// - The second task would deadlock due to the first task holding a lock
// preventing any other CDPAgent tasks from running. (D54838179)
// - The second task would complete first, returning `evaluate`
// responses out of order and (crucially) performing any JS side
// effects out of order. (D55250610)
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {
"result": {
"type": "number",
"value": 3
}
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {
"result": {
"type": "number",
"value": 7
}
}
})"));
});
// Make sure the second task ran last.
EXPECT_EQ(this->eval("globalThis.x").getNumber(), 7);
}
TYPED_TEST(JsiIntegrationHermesTest, ScriptParsedExactlyOnce) {
// Regression test for T182003727 (multiple scriptParsed events for a single
// script under Hermes lazy compilation).
this->connect();
InSequence s;
this->eval(R"(
// NOTE: Triggers lazy compilation in Hermes when running with
// CompilationMode::ForceLazyCompilation.
(function foo(){var x = 2;})()
//# sourceURL=script.js
)");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Debugger.scriptParsed"),
AtJsonPtr("/params/url", "script.js"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Debugger.enable"
})");
}
TYPED_TEST(JsiIntegrationHermesTest, FunctionDescriptionIncludesName) {
// See
// https://github.com/facebook/react-native-devtools-frontend/blob/9a23d4c7c4c2d1a3d9e913af38d6965f474c4284/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts#L311-L391
this->connect();
InSequence s;
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/id", 1),
AtJsonPtr("/result/result/type", "function"),
AtJsonPtr(
"/result/result/description",
DynamicString(StartsWith("function foo() {"))))));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": "(function foo() {Math.random()});"}
})");
}
TYPED_TEST(JsiIntegrationHermesTest, ReleaseRemoteObject) {
this->connect();
InSequence s;
// Create a remote object.
auto objectInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/id", 1),
AtJsonPtr("/result/result/type", "object"),
AtJsonPtr("/result/result/objectId", Not(IsEmpty())))));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": "[]"}
})");
ASSERT_TRUE(objectInfo->has_value());
auto objectId = objectInfo->value()["result"]["result"]["objectId"];
// Ensure we can get the properties of the object.
this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/id", 2), AtJsonPtr("/result/result", SizeIs(Gt(0))))));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 2,
"method": "Runtime.getProperties",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
// Release the object.
this->expectMessageFromPage(JsonEq(R"({
"id": 3,
"result": {}
})"));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 3,
"method": "Runtime.releaseObject",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
// Getting properties for a released object results in an error.
this->expectMessageFromPage(
JsonParsed(AllOf(AtJsonPtr("/id", 4), AtJsonPtr("/error/code", -32000))));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 4,
"method": "Runtime.getProperties",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
// Releasing an already released object is an error.
this->expectMessageFromPage(
JsonParsed(AllOf(AtJsonPtr("/id", 5), AtJsonPtr("/error/code", -32000))));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 5,
"method": "Runtime.releaseObject",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
}
TYPED_TEST(JsiIntegrationHermesTest, ReleaseRemoteObjectGroup) {
this->connect();
InSequence s;
// Create a remote object.
auto objectInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/id", 1),
AtJsonPtr("/result/result/type", "object"),
AtJsonPtr("/result/result/objectId", Not(IsEmpty())))));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": "[]", "objectGroup": "foo"}
})");
ASSERT_TRUE(objectInfo->has_value());
auto objectId = objectInfo->value()["result"]["result"]["objectId"];
// Ensure we can get the properties of the object.
this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/id", 2), AtJsonPtr("/result/result", SizeIs(Gt(0))))));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 2,
"method": "Runtime.getProperties",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
// Release the object group containing our object.
this->expectMessageFromPage(JsonEq(R"({
"id": 3,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 3,
"method": "Runtime.releaseObjectGroup",
"params": {"objectGroup": "foo"}
})");
// Getting properties for a released object results in an error.
this->expectMessageFromPage(
JsonParsed(AllOf(AtJsonPtr("/id", 4), AtJsonPtr("/error/code", -32000))));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 4,
"method": "Runtime.getProperties",
"params": {{"objectId": {}, "ownProperties": true}}
}})",
folly::toJson(objectId)));
// Releasing an already released object group is a no-op.
this->expectMessageFromPage(JsonEq(R"({
"id": 5,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 5,
"method": "Runtime.releaseObjectGroup",
"params": {"objectGroup": "foo"}
})");
}
// A low-level test for captureStackTrace and serializeStackTrace in
// HermesRuntimeTargetDelegate. This functionality is not directly exposed
// to user code, but serves as a building block for higher-level CDP domains.
TYPED_TEST(JsiIntegrationHermesTest, testCaptureAndSerializeStackTrace) {
auto& runtimeTargetDelegate = this->dangerouslyGetRuntimeTargetDelegate();
auto& runtime = this->dangerouslyGetRuntime();
runtime.global().setProperty(
runtime,
"captureCdpStackTrace",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "captureCdpStackTrace"),
0,
[&runtimeTargetDelegate](
jsi::Runtime& rt,
const jsi::Value& /* thisVal */,
const jsi::Value* /* args */,
size_t /* count */) -> jsi::Value {
auto stackTraceDynamic = runtimeTargetDelegate.serializeStackTrace(
*runtimeTargetDelegate.captureStackTrace(rt));
if (!stackTraceDynamic.has_value()) {
return jsi::Value::undefined();
}
return jsi::String::createFromUtf8(
rt, folly::toJson(*stackTraceDynamic));
}));
this->connect();
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Debugger.enable"
})");
auto scriptInfo = this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Debugger.scriptParsed"),
AtJsonPtr("/params/url", "stackTraceTest.js"))));
auto stackTrace = this->eval(R"( // line 0
function inner() { // line 1
return globalThis.captureCdpStackTrace(); // line 2
} // line 3
function outer() { // line 4
return inner(); // line 5
} // line 6
outer(); // line 7
//# sourceURL=stackTraceTest.js
)")
.getString(runtime)
.utf8(runtime);
ASSERT_TRUE(scriptInfo->has_value());
EXPECT_THAT(
stackTrace,
JsonParsed(AllOf(
AtJsonPtr("/callFrames/0/functionName", "inner"),
AtJsonPtr(
"/callFrames/0/scriptId",
scriptInfo->value()["params"]["scriptId"]),
AtJsonPtr("/callFrames/0/lineNumber", 2),
AtJsonPtr("/callFrames/0/columnNumber", 44),
AtJsonPtr("/callFrames/1/functionName", "outer"),
AtJsonPtr(
"/callFrames/1/scriptId",
scriptInfo->value()["params"]["scriptId"]),
AtJsonPtr("/callFrames/1/lineNumber", 5),
AtJsonPtr("/callFrames/1/columnNumber", 18),
AtJsonPtr("/callFrames/2/functionName", "global"),
AtJsonPtr(
"/callFrames/2/scriptId",
scriptInfo->value()["params"]["scriptId"]),
AtJsonPtr("/callFrames/2/lineNumber", 7),
AtJsonPtr("/callFrames/2/columnNumber", 9))));
}
#pragma endregion // AllHermesVariants
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,256 @@
/*
* 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 <gmock/gmock.h>
#include <gtest/gtest.h>
#include <jsinspector-modern/HostTarget.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <memory>
#include <source_location>
#include "FollyDynamicMatchers.h"
#include "GmockHelpers.h"
#include "InspectorMocks.h"
#include "UniquePtrFactory.h"
#include "utils/InspectorFlagOverridesGuard.h"
namespace facebook::react::jsinspector_modern {
/**
* 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, typename Executor>
class JsiIntegrationPortableTestBase : public ::testing::Test, private HostTargetDelegate {
protected:
Executor executor_;
JsiIntegrationPortableTestBase(InspectorFlagOverrides overrides = {})
: inspectorFlagsGuard_(overrides), engineAdapter_{executor_}
{
}
void SetUp() override
{
// NOTE: Using SetUp() so we can call virtual methods like
// setupRuntimeBeforeRegistration().
page_ = HostTarget::create(*this, inspectorExecutor_);
instance_ = &page_->registerInstance(instanceTargetDelegate_);
setupRuntimeBeforeRegistration(engineAdapter_->getRuntime());
runtimeTarget_ =
&instance_->registerRuntime(engineAdapter_->getRuntimeTargetDelegate(), engineAdapter_->getRuntimeExecutor());
loadMainBundle();
}
~JsiIntegrationPortableTestBase() override
{
toPage_.reset();
if (runtimeTarget_ != nullptr) {
EXPECT_TRUE(instance_);
instance_->unregisterRuntime(*runtimeTarget_);
runtimeTarget_ = nullptr;
}
if (instance_ != nullptr) {
page_->unregisterInstance(*instance_);
instance_ = nullptr;
}
}
/**
* Noop in JsiIntegrationPortableTest, but can be overridden by derived
* fixture classes to load some code at startup and after each reload.
*/
virtual void loadMainBundle() {}
/**
* Noop in JsiIntegrationPortableTest, but can be overridden by derived
* fixture classes to set up the runtime before registering it with the
* CDP backend.
*/
virtual void setupRuntimeBeforeRegistration(jsi::Runtime & /*runtime*/) {}
void connect(std::source_location location = std::source_location::current())
{
ASSERT_FALSE(toPage_) << "Can only connect once in a JSI integration test.";
toPage_ = page_->connect(remoteConnections_.make_unique());
using namespace ::testing;
// Default to ignoring console messages originating inside the backend.
EXPECT_CALL_WITH_SOURCE_LOCATION(
location,
fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"), AtJsonPtr("/params/context", "main#InstanceAgent")))))
.Times(AnyNumber());
// We'll always get an onDisconnect call when we tear
// down the test. Expect it in order to satisfy the strict mock.
EXPECT_CALL_WITH_SOURCE_LOCATION(location, *remoteConnections_[0], onDisconnect());
}
void reload()
{
if (runtimeTarget_ != nullptr) {
ASSERT_TRUE(instance_);
instance_->unregisterRuntime(*runtimeTarget_);
runtimeTarget_ = nullptr;
}
if (instance_ != nullptr) {
page_->unregisterInstance(*instance_);
instance_ = nullptr;
}
// Recreate the engine (e.g. to wipe any state in the inner jsi::Runtime)
engineAdapter_.emplace(executor_);
instance_ = &page_->registerInstance(instanceTargetDelegate_);
setupRuntimeBeforeRegistration(engineAdapter_->getRuntime());
runtimeTarget_ =
&instance_->registerRuntime(engineAdapter_->getRuntimeTargetDelegate(), engineAdapter_->getRuntimeExecutor());
loadMainBundle();
}
MockRemoteConnection &fromPage()
{
assert(toPage_);
return *remoteConnections_[0];
}
VoidExecutor inspectorExecutor_ = [this](auto callback) { executor_.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::source_location location = std::source_location::current())
{
using namespace ::testing;
ScopedTrace scope(location.file_name(), location.line(), "");
std::shared_ptr result = std::make_shared<std::optional<folly::dynamic>>(std::nullopt);
EXPECT_CALL_WITH_SOURCE_LOCATION(location, fromPage(), onMessage(matcher))
.WillOnce(([result](auto message) { *result = folly::parseJson(message); }))
.RetiresOnSaturation();
return result;
}
RuntimeTargetDelegate &dangerouslyGetRuntimeTargetDelegate()
{
return engineAdapter_->getRuntimeTargetDelegate();
}
jsi::Runtime &dangerouslyGetRuntime()
{
return engineAdapter_->getRuntime();
}
class SecondaryConnection {
public:
SecondaryConnection(
std::unique_ptr<ILocalConnection> toPage,
JsiIntegrationPortableTestBase<EngineAdapter, Executor> &test,
size_t remoteConnectionIndex)
: toPage_(std::move(toPage)), remoteConnectionIndex_(remoteConnectionIndex), test_(test)
{
}
ILocalConnection &toPage()
{
return *toPage_;
}
MockRemoteConnection &fromPage()
{
return *test_.remoteConnections_[remoteConnectionIndex_];
}
private:
std::unique_ptr<ILocalConnection> toPage_;
size_t remoteConnectionIndex_;
JsiIntegrationPortableTestBase<EngineAdapter, Executor> &test_;
};
SecondaryConnection connectSecondary(std::source_location location = std::source_location::current())
{
auto toPage = page_->connect(remoteConnections_.make_unique());
SecondaryConnection secondary{std::move(toPage), *this, remoteConnections_.objectsVended() - 1};
using namespace ::testing;
// Default to ignoring console messages originating inside the backend.
EXPECT_CALL_WITH_SOURCE_LOCATION(
location,
secondary.fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"), AtJsonPtr("/params/context", "main#InstanceAgent")))))
.Times(AnyNumber());
// We'll always get an onDisconnect call when we tear
// down the test. Expect it in order to satisfy the strict mock.
EXPECT_CALL_WITH_SOURCE_LOCATION(location, secondary.fromPage(), onDisconnect());
return secondary;
}
std::shared_ptr<HostTarget> page_;
InstanceTarget *instance_{};
RuntimeTarget *runtimeTarget_{};
InspectorFlagOverridesGuard inspectorFlagsGuard_;
MockInstanceTargetDelegate instanceTargetDelegate_;
std::optional<EngineAdapter> engineAdapter_;
private:
UniquePtrFactory<::testing::StrictMock<MockRemoteConnection>> remoteConnections_;
protected:
// NOTE: Needs to be destroyed before page_.
std::unique_ptr<ILocalConnection> toPage_;
private:
// HostTargetDelegate methods
HostTargetMetadata getMetadata() override
{
return {.integrationName = "JsiIntegrationTest"};
}
void onReload(const PageReloadRequest &request) override
{
(void)request;
reload();
}
void onSetPausedInDebuggerMessage(const OverlaySetPausedInDebuggerMessageRequest & /*request*/) override {}
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,825 @@
/*
* 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 "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
#include <folly/executors/QueuedImmediateExecutor.h>
#include <jsinspector-modern/InspectorFlags.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/networking/NetworkReporter.h>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
namespace {
struct NetworkReporterTestParams {
bool enableNetworkEventReporting;
};
} // namespace
/**
* A test fixture for the way the internal NetworkReporter API interacts with
* the CDP Network and Tracing domains.
*/
template <typename Params>
requires std::convertible_to<Params, NetworkReporterTestParams>
class NetworkReporterTestBase : public JsiIntegrationPortableTestBase<
JsiIntegrationTestHermesEngineAdapter,
folly::QueuedImmediateExecutor>,
public WithParamInterface<Params> {
protected:
NetworkReporterTestBase()
: JsiIntegrationPortableTestBase({
.networkInspectionEnabled = true,
.enableNetworkEventReporting =
WithParamInterface<Params>::GetParam()
.enableNetworkEventReporting,
}) {}
void SetUp() override {
JsiIntegrationPortableTestBase::SetUp();
connect();
EXPECT_CALL(
fromPage(),
onMessage(
JsonParsed(AllOf(AtJsonPtr("/method", "Debugger.scriptParsed")))))
.Times(AnyNumber())
.WillRepeatedly(Invoke<>([this](const std::string& message) {
auto params = folly::parseJson(message);
// Store the script ID and URL for later use.
scriptUrlsById_.emplace(
params.at("params").at("scriptId").getString(),
params.at("params").at("url").getString());
}));
}
template <typename InnerMatcher>
Matcher<folly::dynamic> ScriptIdMapsTo(InnerMatcher urlMatcher) {
return ResultOf(
[this](const auto& id) { return getScriptUrlById(id.getString()); },
urlMatcher);
}
void startTracing() {
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Tracing.start"
})");
}
/**
* Helper method to end tracing and collect all trace events from potentially
* multiple chunked Tracing.dataCollected messages.
* \returns A vector containing all collected trace events
*/
std::vector<folly::dynamic> endTracingAndCollectEvents() {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
std::vector<folly::dynamic> allTraceEvents;
EXPECT_CALL(
fromPage(),
onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.dataCollected"))))
.Times(AtLeast(1))
.WillRepeatedly(Invoke([&allTraceEvents](const std::string& message) {
auto parsedMessage = folly::parseJson(message);
auto& events = parsedMessage.at("params").at("value");
allTraceEvents.insert(
allTraceEvents.end(),
std::make_move_iterator(events.begin()),
std::make_move_iterator(events.end()));
}));
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Tracing.tracingComplete"),
AtJsonPtr("/params/dataLossOccurred", false))));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Tracing.end"
})");
return allTraceEvents;
}
private:
std::optional<std::string> getScriptUrlById(const std::string& scriptId) {
auto it = scriptUrlsById_.find(scriptId);
if (it == scriptUrlsById_.end()) {
return std::nullopt;
}
return it->second;
}
std::unordered_map<std::string, std::string> scriptUrlsById_;
};
using NetworkReporterTest = NetworkReporterTestBase<NetworkReporterTestParams>;
TEST_P(NetworkReporterTest, testNetworkEnableDisable) {
InSequence s;
EXPECT_FALSE(NetworkReporter::getInstance().isDebuggingEnabled());
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
EXPECT_TRUE(NetworkReporter::getInstance().isDebuggingEnabled());
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
EXPECT_FALSE(NetworkReporter::getInstance().isDebuggingEnabled());
}
TEST_P(NetworkReporterTest, testGetMissingResponseBody) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/error/code", (int)cdp::ErrorCode::InternalError),
AtJsonPtr("/id", 2))));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.getResponseBody",
"params": {
"requestId": "1234567890-no-such-request"
}
})");
this->expectMessageFromPage(JsonEq(R"({
"id": 3,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 3,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testRequestWillBeSentWithRedirect) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSent"),
AtJsonPtr("/params/requestId", "test-request-1"),
AtJsonPtr("/params/loaderId", ""),
AtJsonPtr("/params/documentURL", "mobile"),
AtJsonPtr("/params/request/url", "https://example.com/redirected"),
AtJsonPtr("/params/request/method", "POST"),
AtJsonPtr("/params/request/headers/Content-Type", "application/json"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/wallTime", Gt(0)),
AtJsonPtr("/params/initiator/type", "script"),
AtJsonPtr("/params/redirectHasExtraInfo", true),
AtJsonPtr("/params/redirectResponse", Not(IsEmpty())),
AtJsonPtr("/params/redirectResponse/url", "https://example.com/original"),
AtJsonPtr("/params/redirectResponse/status", 302),
AtJsonPtr(
"/params/redirectResponse/headers/Location",
"https://example.com/redirected"))));
RequestInfo requestInfo;
requestInfo.url = "https://example.com/redirected";
requestInfo.httpMethod = "POST";
requestInfo.headers = Headers{{"Content-Type", "application/json"}};
ResponseInfo redirectResponse;
redirectResponse.url = "https://example.com/original";
redirectResponse.statusCode = 302;
redirectResponse.headers =
Headers{{"Location", "https://example.com/redirected"}};
NetworkReporter::getInstance().reportRequestStart(
"test-request-1", requestInfo, 1024, redirectResponse);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testRequestWillBeSentExtraInfoParameters) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSentExtraInfo"),
AtJsonPtr("/params/requestId", "test-extra-info"),
AtJsonPtr("/params/headers/User-Agent", "TestAgent"),
AtJsonPtr("/params/headers/Accept-Language", "en-US"),
AtJsonPtr("/params/connectTiming/requestTime", Gt(0)))));
Headers extraHeaders = {
{"User-Agent", "TestAgent"}, {"Accept-Language", "en-US"}};
NetworkReporter::getInstance().reportConnectionTiming(
"test-extra-info", extraHeaders);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testLoadingFailedCancelled) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.loadingFailed"),
AtJsonPtr("/params/requestId", "test-request-1"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/type", "Other"),
AtJsonPtr("/params/errorText", "net::ERR_ABORTED"),
AtJsonPtr("/params/canceled", true))));
NetworkReporter::getInstance().reportRequestFailed("test-request-1", true);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testLoadingFailedError) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.loadingFailed"),
AtJsonPtr("/params/requestId", "test-request-1"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/type", "Other"),
AtJsonPtr("/params/errorText", "net::ERR_FAILED"),
AtJsonPtr("/params/canceled", false))));
NetworkReporter::getInstance().reportRequestFailed("test-request-1", false);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testCompleteNetworkFlow) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
const std::string requestId = "complete-flow-request";
// Step 1: Request will be sent
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSent"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/loaderId", ""),
AtJsonPtr("/params/documentURL", "mobile"),
AtJsonPtr("/params/request/url", "https://api.example.com/users"),
AtJsonPtr("/params/request/method", "GET"),
AtJsonPtr("/params/request/headers/Accept", "application/json"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/wallTime", Gt(0)),
AtJsonPtr("/params/initiator/type", "script"),
AtJsonPtr("/params/redirectHasExtraInfo", false))));
RequestInfo requestInfo;
requestInfo.url = "https://api.example.com/users";
requestInfo.httpMethod = "GET";
requestInfo.headers = Headers{{"Accept", "application/json"}};
NetworkReporter::getInstance().reportRequestStart(
requestId, requestInfo, 0, std::nullopt);
// Step 2: Connection timing
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSentExtraInfo"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/headers/Accept", "application/json"),
AtJsonPtr("/params/connectTiming/requestTime", Gt(0)))));
NetworkReporter::getInstance().reportConnectionTiming(
requestId, requestInfo.headers);
// Step 3: Response received
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.responseReceived"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/loaderId", ""),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/type", "XHR"),
AtJsonPtr("/params/response/url", "https://api.example.com/users"),
AtJsonPtr("/params/response/status", 200),
AtJsonPtr("/params/response/statusText", "OK"),
AtJsonPtr("/params/response/headers/Content-Type", "application/json"),
AtJsonPtr("/params/response/headers/Content-Length", "1024"),
AtJsonPtr("/params/response/mimeType", "application/json"),
AtJsonPtr("/params/response/encodedDataLength", 1024),
AtJsonPtr("/params/hasExtraInfo", false))));
ResponseInfo responseInfo;
responseInfo.url = "https://api.example.com/users";
responseInfo.statusCode = 200;
responseInfo.headers =
Headers{{"Content-Type", "application/json"}, {"Content-Length", "1024"}};
NetworkReporter::getInstance().reportResponseStart(
requestId, responseInfo, 1024);
// Step 4: Data received (multiple chunks)
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.dataReceived"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/dataLength", 512),
AtJsonPtr("/params/encodedDataLength", 512))));
NetworkReporter::getInstance().reportDataReceived(requestId, 512, 512);
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.dataReceived"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/dataLength", 512),
AtJsonPtr("/params/encodedDataLength", 512))));
NetworkReporter::getInstance().reportDataReceived(requestId, 512, 512);
// Step 5: Loading finished
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.loadingFinished"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/encodedDataLength", 1024))));
NetworkReporter::getInstance().reportResponseEnd(requestId, 1024);
// Store and retrieve response body
NetworkReporter::getInstance().storeResponseBody(
requestId, R"({"users": [{"id": 1, "name": "John"}]})", false);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {
"body": "{\"users\": [{\"id\": 1, \"name\": \"John\"}]}",
"base64Encoded": false
}
})"));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 2,
"method": "Network.getResponseBody",
"params": {{
"requestId": {0}
}}
}})",
folly::toJson(requestId)));
this->expectMessageFromPage(JsonEq(R"({
"id": 3,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 3,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testGetResponseBodyWithBase64) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
const std::string requestId = "base64-response-test";
// Store base64-encoded response body
NetworkReporter::getInstance().storeResponseBody(
requestId, "SGVsbG8gV29ybGQ=", true);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {
"body": "SGVsbG8gV29ybGQ=",
"base64Encoded": true
}
})"));
this->toPage_->sendMessage(
fmt::format(
R"({{
"id": 2,
"method": "Network.getResponseBody",
"params": {{
"requestId": {0}
}}
}})",
folly::toJson(requestId)));
this->expectMessageFromPage(JsonEq(R"({
"id": 3,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 3,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testNetworkEventsWhenDisabled) {
EXPECT_FALSE(NetworkReporter::getInstance().isDebuggingEnabled());
// NOTE: The test will automatically fail if any unexpected CDP messages are
// received as a result of the following calls.
RequestInfo requestInfo;
requestInfo.url = "https://example.com/disabled";
requestInfo.httpMethod = "GET";
NetworkReporter::getInstance().reportRequestStart(
"disabled-request", requestInfo, 0, std::nullopt);
ResponseInfo responseInfo;
responseInfo.url = "https://example.com/disabled";
responseInfo.statusCode = 200;
NetworkReporter::getInstance().reportConnectionTiming("disabled-request", {});
NetworkReporter::getInstance().reportResponseStart(
"disabled-request", responseInfo, 1024);
NetworkReporter::getInstance().reportDataReceived(
"disabled-request", 512, 512);
NetworkReporter::getInstance().reportResponseEnd("disabled-request", 1024);
NetworkReporter::getInstance().reportRequestFailed("disabled-request", false);
}
TEST_P(NetworkReporterTest, testRequestWillBeSentWithInitiator) {
InSequence s;
this->expectMessageFromPage(JsonEq(R"({
"id": 0,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 0,
"method": "Debugger.enable"
})");
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
RequestInfo requestInfo;
requestInfo.url = "https://example.com/initiator";
requestInfo.httpMethod = "GET";
auto& runtime = engineAdapter_->getRuntime();
auto requestId = this->eval(R"( // line 0
function inner() { // line 1
return globalThis.__NETWORK_REPORTER__.createDevToolsRequestId(); // line 2
} // line 3
function outer() { // line 4
return inner(); // line 5
} // line 6
outer(); // line 7
//# sourceURL=initiatorTest.js
)")
.asString(runtime)
.utf8(runtime);
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSent"),
AtJsonPtr("/params/requestId", requestId),
AtJsonPtr("/params/initiator/type", "script"),
AtJsonPtr(
"/params/initiator/stack/callFrames",
AllOf(
Each(AllOf(
AtJsonPtr("/url", "initiatorTest.js"),
AtJsonPtr(
"/scriptId", this->ScriptIdMapsTo("initiatorTest.js")))),
ElementsAre(
AllOf(
AtJsonPtr("/functionName", "inner"),
AtJsonPtr("/lineNumber", 2)),
AllOf(
AtJsonPtr("/functionName", "outer"),
AtJsonPtr("/lineNumber", 5)),
AllOf(
AtJsonPtr("/functionName", "global"),
AtJsonPtr("/lineNumber", 7))))))));
NetworkReporter::getInstance().reportRequestStart(
requestId, requestInfo, 0, std::nullopt);
this->expectMessageFromPage(JsonEq(R"({
"id": 2,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 2,
"method": "Network.disable"
})");
}
TEST_P(NetworkReporterTest, testCreateRequestIdWithoutNetworkDomain) {
InSequence s;
auto& runtime = engineAdapter_->getRuntime();
auto id1 = this->eval(R"(
globalThis.__NETWORK_REPORTER__.createDevToolsRequestId();
)")
.asString(runtime)
.utf8(runtime);
EXPECT_NE(id1, "");
auto id2 = this->eval(R"(
globalThis.__NETWORK_REPORTER__.createDevToolsRequestId();
)")
.asString(runtime)
.utf8(runtime);
EXPECT_NE(id2, "");
EXPECT_NE(id1, id2);
}
struct NetworkReporterTracingTestParams {
bool enableNetworkEventReporting;
bool enableNetworkDomain;
operator NetworkReporterTestParams() const {
return NetworkReporterTestParams{
.enableNetworkEventReporting = enableNetworkEventReporting,
};
}
};
using NetworkReporterTracingTest =
NetworkReporterTestBase<NetworkReporterTracingTestParams>;
TEST_P(
NetworkReporterTracingTest,
testReportsToTracingDomainPlusNetworkDomain) {
InSequence s;
this->startTracing();
if (GetParam().enableNetworkDomain) {
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Network.enable"
})");
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSent"),
AtJsonPtr("/params/requestId", "trace-events-request"),
AtJsonPtr("/params/loaderId", ""),
AtJsonPtr("/params/documentURL", "mobile"),
AtJsonPtr("/params/request/url", "https://trace.example.com/events"),
AtJsonPtr("/params/request/method", "GET"),
AtJsonPtr("/params/request/headers/Accept", "application/json"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/wallTime", Gt(0)),
AtJsonPtr("/params/initiator/type", "script"),
AtJsonPtr("/params/redirectHasExtraInfo", false))));
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSentExtraInfo"),
AtJsonPtr("/params/requestId", "trace-events-request"),
AtJsonPtr("/params/associatedCookies", "[]"_json),
AtJsonPtr("/params/headers", "{}"_json),
AtJsonPtr("/params/connectTiming/requestTime", Gt(0)))));
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.responseReceived"),
AtJsonPtr("/params/requestId", "trace-events-request"),
AtJsonPtr("/params/loaderId", ""),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/type", "XHR"),
AtJsonPtr("/params/response/url", "https://trace.example.com/events"),
AtJsonPtr("/params/response/status", 200),
AtJsonPtr("/params/response/statusText", "OK"),
AtJsonPtr("/params/response/headers/Content-Type", "application/json"),
AtJsonPtr("/params/response/mimeType", "application/json"),
AtJsonPtr("/params/response/encodedDataLength", 1024),
AtJsonPtr("/params/hasExtraInfo", false))));
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.loadingFinished"),
AtJsonPtr("/params/requestId", "trace-events-request"),
AtJsonPtr("/params/timestamp", Gt(0)),
AtJsonPtr("/params/encodedDataLength", 1024))));
}
NetworkReporter::getInstance().reportRequestStart(
"trace-events-request",
{
.url = "https://trace.example.com/events",
.httpMethod = "GET",
.headers = Headers{{"Accept", "application/json"}},
},
0,
std::nullopt);
NetworkReporter::getInstance().reportConnectionTiming(
"trace-events-request", std::nullopt);
NetworkReporter::getInstance().reportResponseStart(
"trace-events-request",
{
.url = "https://trace.example.com/events",
.statusCode = 200,
.headers = Headers{{"Content-Type", "application/json"}},
},
1024);
NetworkReporter::getInstance().reportResponseEnd(
"trace-events-request", 1024);
auto allTraceEvents = endTracingAndCollectEvents();
EXPECT_THAT(
allTraceEvents,
Contains(AllOf(
AtJsonPtr("/name", "ResourceSendRequest"),
AtJsonPtr("/cat", "devtools.timeline"),
AtJsonPtr("/ph", "I"),
AtJsonPtr("/s", "t"),
AtJsonPtr("/tid", oscompat::getCurrentThreadId()),
AtJsonPtr("/pid", oscompat::getCurrentProcessId()),
AtJsonPtr("/args/data/initiator", "{}"_json),
AtJsonPtr("/args/data/requestId", "trace-events-request"),
AtJsonPtr("/args/data/url", "https://trace.example.com/events"),
AtJsonPtr("/args/data/requestMethod", "GET"),
AtJsonPtr("/args/data/priority", "VeryHigh"),
AtJsonPtr("/args/data/renderBlocking", "non_blocking"),
AtJsonPtr("/args/data/resourceType", "Other"))));
EXPECT_THAT(
allTraceEvents,
Contains(AllOf(
AtJsonPtr("/name", "ResourceReceiveResponse"),
AtJsonPtr("/cat", "devtools.timeline"),
AtJsonPtr("/ph", "I"),
AtJsonPtr("/s", "t"),
AtJsonPtr("/tid", oscompat::getCurrentThreadId()),
AtJsonPtr("/pid", oscompat::getCurrentProcessId()),
AtJsonPtr("/ts", Gt(0)),
AtJsonPtr("/args/data/requestId", "trace-events-request"),
AtJsonPtr("/args/data/statusCode", 200),
AtJsonPtr("/args/data/mimeType", "application/json"),
AtJsonPtr("/args/data/protocol", "h2"),
AtJsonPtr("/args/data/encodedDataLength", 1024),
AtJsonPtr(
"/args/data/headers",
R"([{ "name": "Content-Type", "value": "application/json" }])"_json),
AtJsonPtr(
"/args/data/timing",
AllOf(
AtJsonPtr("/requestTime", Ge(0)),
AtJsonPtr("/sendStart", Ge(0)),
AtJsonPtr("/sendEnd", Ge(0)),
AtJsonPtr("/receiveHeadersStart", Ge(0)),
AtJsonPtr("/receiveHeadersEnd", Ge(0)))))));
EXPECT_THAT(
allTraceEvents,
Contains(AllOf(
AtJsonPtr("/name", "ResourceFinish"),
AtJsonPtr("/cat", "devtools.timeline"),
AtJsonPtr("/ph", "I"),
AtJsonPtr("/s", "t"),
AtJsonPtr("/tid", oscompat::getCurrentThreadId()),
AtJsonPtr("/pid", oscompat::getCurrentProcessId()),
AtJsonPtr("/args/data/requestId", "trace-events-request"),
AtJsonPtr("/args/data/encodedDataLength", 1024),
AtJsonPtr("/args/data/decodedBodyLength", 0),
AtJsonPtr("/args/data/didFail", false))));
}
static const auto networkReporterTestParamValues = testing::Values(
NetworkReporterTestParams{.enableNetworkEventReporting = true},
NetworkReporterTestParams{
.enableNetworkEventReporting = false,
});
static const auto networkReporterTracingTestParamValues = testing::Values(
NetworkReporterTracingTestParams{
.enableNetworkEventReporting = true,
.enableNetworkDomain = true},
NetworkReporterTracingTestParams{
.enableNetworkEventReporting = true,
.enableNetworkDomain = false});
INSTANTIATE_TEST_SUITE_P(
NetworkReporterTest,
NetworkReporterTest,
networkReporterTestParamValues);
INSTANTIATE_TEST_SUITE_P(
NetworkReporterTracingTest,
NetworkReporterTracingTest,
networkReporterTracingTestParamValues);
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,229 @@
/*
* 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 "ReactInstanceIntegrationTest.h"
#include "FollyDynamicMatchers.h"
#include "UniquePtrFactory.h"
#include "prelude.js.h"
#include <folly/json.h>
#include <glog/logging.h>
#include <jsinspector-modern/InspectorFlags.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/runtime/hermes/HermesInstance.h>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
#pragma region ReactInstanceIntegrationTest
ReactInstanceIntegrationTest::ReactInstanceIntegrationTest()
: runtime(nullptr),
instance(nullptr),
messageQueueThread(std::make_shared<MockMessageQueueThread>()),
errorHandler(std::make_shared<ErrorUtils>()),
testMode_(GetParam()) {}
void ReactInstanceIntegrationTest::SetUp() {
// Valdiate that the test mode in InspectorFlags is set correctly
EXPECT_EQ(
InspectorFlags::getInstance().getFuseboxEnabled(),
testMode_ == ReactInstanceIntegrationTestMode::FUSEBOX);
auto mockRegistry = std::make_unique<MockTimerRegistry>();
auto timerManager =
std::make_shared<react::TimerManager>(std::move(mockRegistry));
auto onJsError = [](jsi::Runtime& /*runtime*/,
const JsErrorHandler::ProcessedError& error) noexcept {
LOG(INFO) << "[jsErrorHandlingFunc called]";
LOG(INFO) << error << std::endl;
};
auto jsRuntimeFactory = std::make_unique<react::HermesInstance>();
std::unique_ptr<react::JSRuntime> runtime_ =
jsRuntimeFactory->createJSRuntime(nullptr, messageQueueThread, false);
jsi::Runtime* jsiRuntime = &runtime_->getRuntime();
// Error handler:
jsiRuntime->global().setProperty(
*jsiRuntime,
"ErrorUtils",
jsi::Object::createFromHostObject(*jsiRuntime, errorHandler));
std::shared_ptr<HostTarget> hostTargetIfModernCDP = nullptr;
if (InspectorFlags::getInstance().getFuseboxEnabled()) {
VoidExecutor inspectorExecutor = [this](auto callback) {
immediateExecutor_.add(callback);
};
hostTargetIfModernCDP =
HostTarget::create(hostTargetDelegate_, inspectorExecutor);
}
instance = std::make_unique<react::ReactInstance>(
std::move(runtime_),
messageQueueThread,
timerManager,
std::move(onJsError),
hostTargetIfModernCDP == nullptr ? nullptr : hostTargetIfModernCDP.get());
timerManager->setRuntimeExecutor(instance->getBufferedRuntimeExecutor());
// JS Environment:
initializeRuntime(preludeJsCode);
// Inspector:
auto& inspector = getInspectorInstance();
ASSERT_NE(hostTargetIfModernCDP, nullptr);
// Under modern CDP, the React host is responsible for adding itself as
// the root target on startup.
pageId_ = inspector.addPage(
"mock-description",
"mock-vm",
[hostTargetIfModernCDP](std::unique_ptr<IRemoteConnection> remote)
-> std::unique_ptr<ILocalConnection> {
auto localConnection =
hostTargetIfModernCDP->connect(std::move(remote));
return localConnection;
},
// TODO: Allow customisation of InspectorTargetCapabilities
{});
clientToVM_ =
inspector.connect(pageId_.value(), mockRemoteConnections_.make_unique());
ASSERT_NE(clientToVM_, nullptr);
// Default to ignoring console messages originating inside the backend.
EXPECT_CALL(
getRemoteConnection(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"),
AtJsonPtr("/params/context", "main#InstanceAgent")))))
.Times(AnyNumber());
}
void ReactInstanceIntegrationTest::TearDown() {
clientToVM_->disconnect();
// Destroy the local connection.
clientToVM_.reset();
if (pageId_.has_value() &&
InspectorFlags::getInstance().getFuseboxEnabled()) {
// Under modern CDP, clean up the page we added in SetUp and destroy
// resources owned by HostTarget.
getInspectorInstance().removePage(pageId_.value());
}
pageId_.reset();
// Expect the remote connection to have been destroyed.
EXPECT_EQ(mockRemoteConnections_[0], nullptr);
// Make sure that any dangerous overriding is removed before the next test.
// Seemingly, we need both of these to cleanly reset and not break subsequent
// tests.
InspectorFlags::getInstance().dangerouslyResetFlags();
ReactNativeFeatureFlags::dangerouslyReset();
}
void ReactInstanceIntegrationTest::initializeRuntime(std::string_view script) {
react::ReactInstance::JSRuntimeFlags flags{
.isProfiling = false,
};
instance->initializeRuntime(flags, [](jsi::Runtime& rt) {
// NOTE: RN's console polyfill (included in prelude.js.h) depends on the
// native logging hook being installed, even if it's a noop.
facebook::react::bindNativeLogger(rt, [](auto, auto) {});
});
messageQueueThread->tick();
std::string init(script);
// JS calls no longer buffered after calling loadScript
instance->loadScript(std::make_unique<react::JSBigStdString>(init), "");
}
void ReactInstanceIntegrationTest::send(
const std::string& method,
const folly::dynamic& params) {
folly::dynamic request = folly::dynamic::object();
request["method"] = method;
request["id"] = id_++;
request["params"] = params;
sendJSONString(folly::toJson(request));
}
void ReactInstanceIntegrationTest::sendJSONString(const std::string& message) {
// The runtime must be initialized and connected to before messaging
clientToVM_->sendMessage(message);
}
jsi::Value ReactInstanceIntegrationTest::run(const std::string& script) {
auto runtimeExecutor = instance->getUnbufferedRuntimeExecutor();
auto ret = jsi::Value::undefined();
runtimeExecutor([script, &ret](jsi::Runtime& rt) {
ret = rt.evaluateJavaScript(
std::make_unique<jsi::StringBuffer>(script), "<test>");
});
messageQueueThread->flush();
while (verbose_ && errorHandler->size() > 0) {
LOG(INFO) << "Error: " << errorHandler->getLastError().getMessage();
}
return ret;
}
bool ReactInstanceIntegrationTest::verbose(bool isVerbose) {
const bool previous = verbose_;
verbose_ = isVerbose;
return previous;
}
#pragma endregion
TEST_P(ReactInstanceIntegrationTest, RuntimeEvalTest) {
auto val = run("1 + 2");
EXPECT_EQ(val.asNumber(), 3);
}
TEST_P(ReactInstanceIntegrationTest, ConsoleLog) {
EXPECT_CALL(
getRemoteConnection(),
onMessage(JsonParsed(
AtJsonPtr("/method", Eq("Runtime.executionContextCreated")))));
EXPECT_CALL(
getRemoteConnection(), onMessage(JsonParsed(AtJsonPtr("/id", Eq(1)))));
InSequence s;
EXPECT_CALL(
getRemoteConnection(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/params/args/0/value", Eq("Hello, World!")),
AtJsonPtr("/method", Eq("Runtime.consoleAPICalled"))))));
EXPECT_CALL(getRemoteConnection(), onDisconnect());
send("Runtime.enable");
run("console.log('Hello, World!');");
}
INSTANTIATE_TEST_SUITE_P(
ReactInstanceVaryingInspectorBackend,
ReactInstanceIntegrationTest,
::testing::Values(ReactInstanceIntegrationTestMode::FUSEBOX));
} // 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.
*/
#pragma once
#include "InspectorMocks.h"
#include "ReactNativeMocks.h"
#include "UniquePtrFactory.h"
#include <folly/executors/QueuedImmediateExecutor.h>
#include <folly/json.h>
#include <gtest/gtest.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <cassert>
#include <memory>
namespace facebook::react::jsinspector_modern {
using namespace ::testing;
enum ReactInstanceIntegrationTestMode {
FUSEBOX,
};
class ReactInstanceIntegrationTest : public Test,
public ::testing::WithParamInterface<ReactInstanceIntegrationTestMode> {
protected:
ReactInstanceIntegrationTest();
void SetUp() override;
void TearDown() override;
jsi::Value run(const std::string &script);
bool verbose(bool isVerbose);
void send(const std::string &method, const folly::dynamic &params = folly::dynamic::object());
void sendJSONString(const std::string &message);
jsi::Runtime *runtime;
std::unique_ptr<react::ReactInstance> instance;
std::shared_ptr<MockMessageQueueThread> messageQueueThread;
std::shared_ptr<ErrorUtils> errorHandler;
NiceMock<MockRemoteConnection> &getRemoteConnection()
{
EXPECT_EQ(mockRemoteConnections_.objectsVended(), 1);
auto rawPtr = mockRemoteConnections_[0];
assert(rawPtr);
return *rawPtr;
}
private:
void initializeRuntime(std::string_view script);
ReactInstanceIntegrationTestMode testMode_;
size_t id_ = 1;
bool verbose_ = false;
std::optional<int> pageId_;
UniquePtrFactory<NiceMock<MockRemoteConnection>> mockRemoteConnections_;
std::unique_ptr<ILocalConnection> clientToVM_;
folly::QueuedImmediateExecutor immediateExecutor_;
MockHostTargetDelegate hostTargetDelegate_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,94 @@
/*
* 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 "ReactNativeMocks.h"
#include <glog/logging.h>
namespace facebook::react::jsinspector_modern {
//
// MockMessageQueueThread
//
void MockMessageQueueThread::runOnQueue(std::function<void()>&& func) {
callbackQueue_.push(func);
}
void MockMessageQueueThread::tick() {
if (!callbackQueue_.empty()) {
auto callback = callbackQueue_.front();
callback();
callbackQueue_.pop();
}
}
void MockMessageQueueThread::guardedTick() {
try {
tick();
} catch (const std::exception& e) {
// For easier debugging
FAIL() << e.what();
}
}
void MockMessageQueueThread::flush() {
while (!callbackQueue_.empty()) {
tick();
}
}
size_t MockMessageQueueThread::size() {
return callbackQueue_.size();
}
void MockMessageQueueThread::quitSynchronous() {
assert(false && "Not implemented");
}
void MockMessageQueueThread::runOnQueueSync(std::function<void()>&& callback) {
callback();
}
//
// ErrorUtils
//
jsi::Value ErrorUtils::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
auto methodName = name.utf8(rt);
if (methodName == "reportFatalError") {
return jsi::Function::createFromHostFunction(
rt,
name,
1,
[this](
jsi::Runtime& runtime,
/* thisValue */ const jsi::Value&,
const jsi::Value* arguments,
size_t count) {
if (count >= 1) {
auto value = jsi::Value(runtime, arguments[0]);
auto error = jsi::JSError(runtime, std::move(value));
LOG(INFO) << "JSI Fatal: " << error.getMessage();
reportFatalError(std::move(error));
}
return jsi::Value::undefined();
});
} else {
throw std::runtime_error("Unknown method: " + methodName);
}
}
size_t ErrorUtils::size() {
return errors_.size();
}
jsi::JSError ErrorUtils::getLastError() {
auto error = errors_.back();
errors_.pop_back();
return error;
}
} // 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 <gmock/gmock.h>
#include <gtest/gtest.h>
#include <queue>
#include <ReactCommon/RuntimeExecutor.h>
#include <jsi/jsi.h>
#include <react/runtime/ReactInstance.h>
namespace facebook::react::jsinspector_modern {
class MockTimerRegistry : public react::PlatformTimerRegistry {
public:
MOCK_METHOD2(createTimer, void(uint32_t, double));
MOCK_METHOD2(createRecurringTimer, void(uint32_t, double));
MOCK_METHOD1(deleteTimer, void(uint32_t));
};
class MockMessageQueueThread : public react::MessageQueueThread {
public:
void runOnQueue(std::function<void()> &&func) override;
// Unused
void runOnQueueSync(std::function<void()> &&callback) override;
// Unused
void quitSynchronous() override;
void tick();
void flush();
void guardedTick();
size_t size();
private:
std::queue<std::function<void()>> callbackQueue_;
};
class ErrorUtils : public jsi::HostObject {
public:
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override;
void reportFatalError(jsi::JSError &&error)
{
errors_.push_back(std::move(error));
}
size_t size();
jsi::JSError getLastError();
private:
std::vector<jsi::JSError> errors_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,111 @@
/*
* 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,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 <jsinspector-modern/Utf8.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
TEST(Utf8Test, TruncateToValidUtf8) {
auto buffer = std::vector<char>();
std::vector<std::pair<std::string, size_t>> expectedStringsUpToSizes;
// Construct a buffer with a concatenation of all code points, and a vector
// or "expectedStringsUpToSizes", pairs of valid UTF8 prefix strings and sizes
// "n" such that the string would be the expected truncation of the first "n"
// bytes of the buffer.
for (const std::string& codePoint : {
"a", // 1 byte
"é", // 2 bytes
"", // 3 bytes
"😀" // 4 bytes
}) {
auto partial = std::string(buffer.data(), buffer.size());
buffer.insert(buffer.end(), codePoint.begin(), codePoint.end());
expectedStringsUpToSizes.push_back(std::pair(partial, buffer.size()));
}
// The constructed buffer is 10 bytes long, comprised of 4 code points of
// varied size. Range over naive slices of length 0-9 ensuring that the
// truncated result matches the valid UTF8 substring of length <= n.
size_t n = 0;
for (const auto& expectedStringUpToSize : expectedStringsUpToSizes) {
auto nextSize = expectedStringUpToSize.second;
auto expectedString = expectedStringUpToSize.first;
for (; n < nextSize; ++n) {
// Take the first n bytes of the whole buffer, which may be slicing
// through the middle of a code point.
std::vector<char> slice(buffer.begin(), buffer.begin() + n);
truncateToValidUTF8(slice);
// Expect the final code point fragment has been discarded and that the
// contents are equal to expectedString, which is valid UTF8.
EXPECT_EQ(std::string(slice.begin(), slice.end()), expectedString);
}
}
// Finally verify that truncating the whole buffer, which is already valid
// UTF8, is a no-op.
auto wholeString = std::string(buffer.begin(), buffer.end());
truncateToValidUTF8(buffer);
EXPECT_EQ(std::string(buffer.begin(), buffer.end()), wholeString);
}
} // namespace facebook::react::jsinspector_modern

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,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 <folly/executors/QueuedImmediateExecutor.h>
#include <hermes/hermes.h>
#include "JsiIntegrationTestGenericEngineAdapter.h"
namespace facebook::react::jsinspector_modern {
JsiIntegrationTestGenericEngineAdapter::JsiIntegrationTestGenericEngineAdapter(
folly::Executor& jsExecutor)
: runtime_{hermes::makeHermesRuntime()},
jsExecutor_{jsExecutor},
runtimeTargetDelegate_{
"Generic engine (" + runtime_->description() + ")"} {}
RuntimeTargetDelegate&
JsiIntegrationTestGenericEngineAdapter::getRuntimeTargetDelegate() {
return runtimeTargetDelegate_;
}
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,45 @@
/*
* 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 "../utils/InspectorFlagOverridesGuard.h"
#include <jsinspector-modern/FallbackRuntimeTargetDelegate.h>
#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:
explicit JsiIntegrationTestGenericEngineAdapter(folly::Executor &jsExecutor);
static InspectorFlagOverrides getInspectorFlagOverrides() noexcept;
RuntimeTargetDelegate &getRuntimeTargetDelegate();
jsi::Runtime &getRuntime() const noexcept;
RuntimeExecutor getRuntimeExecutor() const noexcept;
private:
std::unique_ptr<jsi::Runtime> runtime_;
folly::Executor &jsExecutor_;
jsinspector_modern::FallbackRuntimeTargetDelegate runtimeTargetDelegate_;
};
} // 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.
*/
#include "JsiIntegrationTestHermesEngineAdapter.h"
namespace facebook::react::jsinspector_modern {
JsiIntegrationTestHermesEngineAdapter::JsiIntegrationTestHermesEngineAdapter(
folly::Executor& jsExecutor)
: runtime_{hermes::makeHermesRuntime(
::hermes::vm::RuntimeConfig::Builder()
.withCompilationMode(
::hermes::vm::CompilationMode::ForceLazyCompilation)
.build())},
jsExecutor_{jsExecutor},
runtimeTargetDelegate_{runtime_} {
// NOTE: In React Native, registerForProfiling is called by
// HermesInstance::unstable_initializeOnJsThread, called from
// ReactInstance::initializeRuntime. Ideally, we should figure out how to
// manages this from inside the CDP backend,
runtime_->registerForProfiling();
}
RuntimeTargetDelegate&
JsiIntegrationTestHermesEngineAdapter::getRuntimeTargetDelegate() {
return runtimeTargetDelegate_;
}
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,45 @@
/*
* 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 "../utils/InspectorFlagOverridesGuard.h"
#include <jsinspector-modern/RuntimeTarget.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <hermes/hermes.h>
#include <hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h>
#include <jsi/jsi.h>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* An engine adapter for JsiIntegrationTest that uses Hermes (and Hermes'
* modern CDPAgent API).
*/
class JsiIntegrationTestHermesEngineAdapter {
public:
explicit JsiIntegrationTestHermesEngineAdapter(folly::Executor &jsExecutor);
static InspectorFlagOverrides getInspectorFlagOverrides() noexcept;
RuntimeTargetDelegate &getRuntimeTargetDelegate();
jsi::Runtime &getRuntime() const noexcept;
RuntimeExecutor getRuntimeExecutor() const noexcept;
private:
std::shared_ptr<facebook::hermes::HermesRuntime> runtime_;
folly::Executor &jsExecutor_;
HermesRuntimeTargetDelegate runtimeTargetDelegate_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,636 @@
/**
* 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.
*
* @polyfill
* @nolint
* @format
*/
/* eslint-disable no-shadow, eqeqeq, curly, no-unused-vars, no-void, no-control-regex */
#pragma once
constexpr std::string_view preludeJsCode = R"___(
(function (global, __DEV__) {const inspect = (function () {
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// https://github.com/joyent/node/blob/master/lib/util.js
function inspect(obj, opts) {
var ctx = {
seen: [],
formatValueCalls: 0,
stylize: stylizeNoColor,
};
return formatValue(ctx, obj, opts.depth);
}
function stylizeNoColor(str, styleType) {
return str;
}
function arrayToHash(array) {
var hash = {};
array.forEach(function (val, idx) {
hash[val] = true;
});
return hash;
}
function formatValue(ctx, value, recurseTimes) {
ctx.formatValueCalls++;
if (ctx.formatValueCalls > 200) {
return `[TOO BIG formatValueCalls ${ctx.formatValueCalls} exceeded limit of 200]`;
}
// Primitive types cannot have properties
var primitive = formatPrimitive(ctx, value);
if (primitive) {
return primitive;
}
// Look up the keys of the object.
var keys = Object.keys(value);
var visibleKeys = arrayToHash(keys);
// IE doesn't make error fields non-enumerable
// http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
if (
isError(value) &&
(keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)
) {
return formatError(value);
}
// Some type of object without properties can be shortcutted.
if (keys.length === 0) {
if (isFunction(value)) {
var name = value.name ? ': ' + value.name : '';
return ctx.stylize('[Function' + name + ']', 'special');
}
if (isRegExp(value)) {
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
}
if (isDate(value)) {
return ctx.stylize(Date.prototype.toString.call(value), 'date');
}
if (isError(value)) {
return formatError(value);
}
}
var base = '',
array = false,
braces = ['{', '}'];
// Make Array say that they are Array
if (isArray(value)) {
array = true;
braces = ['[', ']'];
}
// Make functions say that they are functions
if (isFunction(value)) {
var n = value.name ? ': ' + value.name : '';
base = ' [Function' + n + ']';
}
// Make RegExps say that they are RegExps
if (isRegExp(value)) {
base = ' ' + RegExp.prototype.toString.call(value);
}
// Make dates with properties first say the date
if (isDate(value)) {
base = ' ' + Date.prototype.toUTCString.call(value);
}
// Make error with message first say the error
if (isError(value)) {
base = ' ' + formatError(value);
}
if (keys.length === 0 && (!array || value.length == 0)) {
return braces[0] + base + braces[1];
}
if (recurseTimes < 0) {
if (isRegExp(value)) {
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
} else {
return ctx.stylize('[Object]', 'special');
}
}
ctx.seen.push(value);
var output;
if (array) {
output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
} else {
output = keys.map(function (key) {
return formatProperty(
ctx,
value,
recurseTimes,
visibleKeys,
key,
array,
);
});
}
ctx.seen.pop();
return reduceToSingleString(output, base, braces);
}
function formatPrimitive(ctx, value) {
if (isUndefined(value)) return ctx.stylize('undefined', 'undefined');
if (isString(value)) {
var simple =
"'" +
JSON.stringify(value)
.replace(/^"|"$/g, '')
.replace(/'/g, "\\'")
.replace(/\\"/g, '"') +
"'";
return ctx.stylize(simple, 'string');
}
if (isNumber(value)) return ctx.stylize('' + value, 'number');
if (isBoolean(value)) return ctx.stylize('' + value, 'boolean');
// For some reason typeof null is "object", so special case here.
if (isNull(value)) return ctx.stylize('null', 'null');
}
function formatError(value) {
return '[' + Error.prototype.toString.call(value) + ']';
}
function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
var output = [];
for (var i = 0, l = value.length; i < l; ++i) {
if (hasOwnProperty(value, String(i))) {
output.push(
formatProperty(
ctx,
value,
recurseTimes,
visibleKeys,
String(i),
true,
),
);
} else {
output.push('');
}
}
keys.forEach(function (key) {
if (!key.match(/^\d+$/)) {
output.push(
formatProperty(ctx, value, recurseTimes, visibleKeys, key, true),
);
}
});
return output;
}
function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
var name, str, desc;
desc = Object.getOwnPropertyDescriptor(value, key) || {value: value[key]};
if (desc.get) {
if (desc.set) {
str = ctx.stylize('[Getter/Setter]', 'special');
} else {
str = ctx.stylize('[Getter]', 'special');
}
} else {
if (desc.set) {
str = ctx.stylize('[Setter]', 'special');
}
}
if (!hasOwnProperty(visibleKeys, key)) {
name = '[' + key + ']';
}
if (!str) {
if (ctx.seen.indexOf(desc.value) < 0) {
if (isNull(recurseTimes)) {
str = formatValue(ctx, desc.value, null);
} else {
str = formatValue(ctx, desc.value, recurseTimes - 1);
}
if (str.indexOf('\n') > -1) {
if (array) {
str = str
.split('\n')
.map(function (line) {
return ' ' + line;
})
.join('\n')
.slice(2);
} else {
str =
'\n' +
str
.split('\n')
.map(function (line) {
return ' ' + line;
})
.join('\n');
}
}
} else {
str = ctx.stylize('[Circular]', 'special');
}
}
if (isUndefined(name)) {
if (array && key.match(/^\d+$/)) {
return str;
}
name = JSON.stringify('' + key);
if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
name = name.slice(1, name.length - 1);
name = ctx.stylize(name, 'name');
} else {
name = name
.replace(/'/g, "\\'")
.replace(/\\"/g, '"')
.replace(/(^"|"$)/g, "'");
name = ctx.stylize(name, 'string');
}
}
return name + ': ' + str;
}
function reduceToSingleString(output, base, braces) {
var numLinesEst = 0;
var length = output.reduce(function (prev, cur) {
numLinesEst++;
if (cur.indexOf('\n') >= 0) numLinesEst++;
return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
}, 0);
if (length > 60) {
return (
braces[0] +
(base === '' ? '' : base + '\n ') +
' ' +
output.join(',\n ') +
' ' +
braces[1]
);
}
return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
}
// NOTE: These type checking functions intentionally don't use `instanceof`
// because it is fragile and can be easily faked with `Object.create()`.
function isArray(ar) {
return Array.isArray(ar);
}
function isBoolean(arg) {
return typeof arg === 'boolean';
}
function isNull(arg) {
return arg === null;
}
function isNullOrUndefined(arg) {
return arg == null;
}
function isNumber(arg) {
return typeof arg === 'number';
}
function isString(arg) {
return typeof arg === 'string';
}
function isSymbol(arg) {
return typeof arg === 'symbol';
}
function isUndefined(arg) {
return arg === void 0;
}
function isRegExp(re) {
return isObject(re) && objectToString(re) === '[object RegExp]';
}
function isObject(arg) {
return typeof arg === 'object' && arg !== null;
}
function isDate(d) {
return isObject(d) && objectToString(d) === '[object Date]';
}
function isError(e) {
return (
isObject(e) &&
(objectToString(e) === '[object Error]' || e instanceof Error)
);
}
function isFunction(arg) {
return typeof arg === 'function';
}
function objectToString(o) {
return Object.prototype.toString.call(o);
}
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
return inspect;
})();
const OBJECT_COLUMN_NAME = '(index)';
const LOG_LEVELS = {
trace: 0,
info: 1,
warn: 2,
error: 3,
};
const INSPECTOR_LEVELS = [];
INSPECTOR_LEVELS[LOG_LEVELS.trace] = 'debug';
INSPECTOR_LEVELS[LOG_LEVELS.info] = 'log';
INSPECTOR_LEVELS[LOG_LEVELS.warn] = 'warning';
INSPECTOR_LEVELS[LOG_LEVELS.error] = 'error';
// Strip the inner function in getNativeLogFunction(), if in dev also
// strip method printing to originalConsole.
const INSPECTOR_FRAMES_TO_SKIP = __DEV__ ? 2 : 1;
function getNativeLogFunction(level) {
return function () {
let str;
if (arguments.length === 1 && typeof arguments[0] === 'string') {
str = arguments[0];
} else {
str = Array.prototype.map
.call(arguments, function (arg) {
return inspect(arg, {depth: 10});
})
.join(', ');
}
// TRICKY
// If more than one argument is provided, the code above collapses them all
// into a single formatted string. This transform wraps string arguments in
// single quotes (e.g. "foo" -> "'foo'") which then breaks the "Warning:"
// check below. So it's important that we look at the first argument, rather
// than the formatted argument string.
const firstArg = arguments[0];
let logLevel = level;
if (
typeof firstArg === 'string' &&
firstArg.slice(0, 9) === 'Warning: ' &&
logLevel >= LOG_LEVELS.error
) {
// React warnings use console.error so that a stack trace is shown,
// but we don't (currently) want these to show a redbox
// (Note: Logic duplicated in ExceptionsManager.js.)
logLevel = LOG_LEVELS.warn;
}
if (global.__inspectorLog) {
global.__inspectorLog(
INSPECTOR_LEVELS[logLevel],
str,
[].slice.call(arguments),
INSPECTOR_FRAMES_TO_SKIP,
);
}
if (groupStack.length) {
str = groupFormat('', str);
}
global.nativeLoggingHook(str, logLevel);
};
}
function repeat(element, n) {
return Array.apply(null, Array(n)).map(function () {
return element;
});
}
function consoleTablePolyfill(rows) {
// convert object -> array
if (!Array.isArray(rows)) {
var data = rows;
rows = [];
for (var key in data) {
if (data.hasOwnProperty(key)) {
var row = data[key];
row[OBJECT_COLUMN_NAME] = key;
rows.push(row);
}
}
}
if (rows.length === 0) {
global.nativeLoggingHook('', LOG_LEVELS.info);
return;
}
var columns = Object.keys(rows[0]).sort();
var stringRows = [];
var columnWidths = [];
// Convert each cell to a string. Also
// figure out max cell width for each column
columns.forEach(function (k, i) {
columnWidths[i] = k.length;
for (var j = 0; j < rows.length; j++) {
var cellStr = (rows[j][k] || '?').toString();
stringRows[j] = stringRows[j] || [];
stringRows[j][i] = cellStr;
columnWidths[i] = Math.max(columnWidths[i], cellStr.length);
}
});
// Join all elements in the row into a single string with | separators
// (appends extra spaces to each cell to make separators | aligned)
function joinRow(row, space) {
var cells = row.map(function (cell, i) {
var extraSpaces = repeat(' ', columnWidths[i] - cell.length).join('');
return cell + extraSpaces;
});
space = space || ' ';
return cells.join(space + '|' + space);
}
var separators = columnWidths.map(function (columnWidth) {
return repeat('-', columnWidth).join('');
});
var separatorRow = joinRow(separators, '-');
var header = joinRow(columns);
var table = [header, separatorRow];
for (var i = 0; i < rows.length; i++) {
table.push(joinRow(stringRows[i]));
}
// Notice extra empty line at the beginning.
// Native logging hook adds "RCTLog >" at the front of every
// logged string, which would shift the header and screw up
// the table
global.nativeLoggingHook('\n' + table.join('\n'), LOG_LEVELS.info);
}
const GROUP_PAD = '\u2502'; // Box light vertical
const GROUP_OPEN = '\u2510'; // Box light down+left
const GROUP_CLOSE = '\u2518'; // Box light up+left
const groupStack = [];
function groupFormat(prefix, msg) {
// Insert group formatting before the console message
return groupStack.join('') + prefix + ' ' + (msg || '');
}
function consoleGroupPolyfill(label) {
global.nativeLoggingHook(groupFormat(GROUP_OPEN, label), LOG_LEVELS.info);
groupStack.push(GROUP_PAD);
}
function consoleGroupCollapsedPolyfill(label) {
global.nativeLoggingHook(groupFormat(GROUP_CLOSE, label), LOG_LEVELS.info);
groupStack.push(GROUP_PAD);
}
function consoleGroupEndPolyfill() {
groupStack.pop();
global.nativeLoggingHook(groupFormat(GROUP_CLOSE), LOG_LEVELS.info);
}
function consoleAssertPolyfill(expression, label) {
if (!expression) {
global.nativeLoggingHook('Assertion failed: ' + label, LOG_LEVELS.error);
}
}
if (global.nativeLoggingHook) {
const originalConsole = global.console;
// Preserve the original `console` as `originalConsole`
if (__DEV__ && originalConsole) {
const descriptor = Object.getOwnPropertyDescriptor(global, 'console');
if (descriptor) {
Object.defineProperty(global, 'originalConsole', descriptor);
}
}
global.console = {
...(originalConsole ?? {}),
error: getNativeLogFunction(LOG_LEVELS.error),
info: getNativeLogFunction(LOG_LEVELS.info),
log: getNativeLogFunction(LOG_LEVELS.info),
warn: getNativeLogFunction(LOG_LEVELS.warn),
trace: getNativeLogFunction(LOG_LEVELS.trace),
debug: getNativeLogFunction(LOG_LEVELS.trace),
table: consoleTablePolyfill,
group: consoleGroupPolyfill,
groupEnd: consoleGroupEndPolyfill,
groupCollapsed: consoleGroupCollapsedPolyfill,
assert: consoleAssertPolyfill,
};
Object.defineProperty(console, '_isPolyfilled', {
value: true,
enumerable: false,
});
// If available, also call the original `console` method since that is
// sometimes useful. Ex: on OS X, this will let you see rich output in
// the Safari Web Inspector console.
if (__DEV__ && originalConsole) {
Object.keys(console).forEach(methodName => {
const reactNativeMethod = console[methodName];
if (
originalConsole[methodName] &&
reactNativeMethod !== originalConsole[methodName]
) {
console[methodName] = function () {
originalConsole[methodName](...arguments);
reactNativeMethod.apply(console, arguments);
};
}
});
// The following methods are not supported by this polyfill but
// we still should pass them to original console if they are
// supported by it.
['clear', 'dir', 'dirxml', 'profile', 'profileEnd'].forEach(methodName => {
if (typeof originalConsole[methodName] === 'function') {
console[methodName] = function () {
originalConsole[methodName](...arguments);
};
}
});
}
} else if (!global.console) {
function stub() {}
const log = global.print || stub;
global.console = {
debug: log,
error: log,
info: log,
log: log,
trace: log,
warn: log,
assert(expression, label) {
if (!expression) {
log('Assertion failed: ' + label);
}
},
clear: stub,
dir: stub,
dirxml: stub,
group: stub,
groupCollapsed: stub,
groupEnd: stub,
profile: stub,
profileEnd: stub,
table: stub,
};
Object.defineProperty(console, '_isPolyfilled', {
value: true,
enumerable: false,
});
}})(globalThis, true)
//# sourceURL=prelude.js
)___";

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 "InspectorFlagOverridesGuard.h"
#include <jsinspector-modern/InspectorFlags.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* Helper class that maps \c InspectorFlagOverrides to the shape of \c
* ReactNativeFeatureFlagsDefaults.
*/
class ReactNativeFeatureFlagsOverrides
: public ReactNativeFeatureFlagsDefaults {
public:
explicit ReactNativeFeatureFlagsOverrides(
const InspectorFlagOverrides& overrides)
: overrides_(overrides) {}
bool fuseboxEnabledRelease() override {
return overrides_.fuseboxEnabledRelease.value_or(
ReactNativeFeatureFlagsDefaults::fuseboxEnabledRelease());
}
bool fuseboxNetworkInspectionEnabled() override {
return overrides_.networkInspectionEnabled.value_or(
ReactNativeFeatureFlagsDefaults::fuseboxNetworkInspectionEnabled());
}
bool enableBridgelessArchitecture() override {
// NOTE: Network support is gated by (enableBridgelessArchitecture &&
// fuseboxNetworkInspectionEnabled).
return overrides_.networkInspectionEnabled.value_or(
ReactNativeFeatureFlagsDefaults::enableBridgelessArchitecture());
}
bool enableNetworkEventReporting() override {
return overrides_.enableNetworkEventReporting.value_or(
ReactNativeFeatureFlagsDefaults::enableNetworkEventReporting());
}
private:
InspectorFlagOverrides overrides_;
};
InspectorFlagOverridesGuard::InspectorFlagOverridesGuard(
const InspectorFlagOverrides& overrides) {
InspectorFlags::getInstance().dangerouslyResetFlags();
ReactNativeFeatureFlags::override(
std::make_unique<ReactNativeFeatureFlagsOverrides>(overrides));
}
InspectorFlagOverridesGuard::~InspectorFlagOverridesGuard() {
ReactNativeFeatureFlags::dangerouslyReset();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,42 @@
/*
* 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 <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
#include <optional>
namespace facebook::react::jsinspector_modern {
/**
* Overridden \c InspectorFlags values for use in tests.
*/
struct InspectorFlagOverrides {
// NOTE: Keep these entries in sync with ReactNativeFeatureFlagsOverrides in
// the implementation file.
std::optional<bool> fuseboxEnabledRelease;
std::optional<bool> networkInspectionEnabled;
std::optional<bool> enableNetworkEventReporting;
};
/**
* A RAII helper to set up and tear down \c InspectorFlags (via \c
* ReactNativeFeatureFlags) with overrides for the lifetime of a test object.
*/
class InspectorFlagOverridesGuard {
public:
explicit InspectorFlagOverridesGuard(const InspectorFlagOverrides &overrides);
InspectorFlagOverridesGuard(const InspectorFlagOverridesGuard &) = delete;
InspectorFlagOverridesGuard(InspectorFlagOverridesGuard &&) = default;
InspectorFlagOverridesGuard &operator=(const InspectorFlagOverridesGuard &) = delete;
InspectorFlagOverridesGuard &operator=(InspectorFlagOverridesGuard &&) = default;
~InspectorFlagOverridesGuard();
};
} // namespace facebook::react::jsinspector_modern

Some files were not shown because too many files have changed in this diff Show More