Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<e59e70d753403b168558b0d2b9513b5c>>
* @generated SignedSource<<4fda339ba0682c3d729725e20a016d27>>
*/

/**
Expand Down Expand Up @@ -41,9 +41,9 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun inspectorEnableCxxInspectorPackagerConnection(): Boolean = false

override fun inspectorEnableHermesCDPAgent(): Boolean = false
override fun inspectorEnableHermesCDPAgent(): Boolean = true

override fun inspectorEnableModernCDPRegistry(): Boolean = false
override fun inspectorEnableModernCDPRegistry(): Boolean = true

override fun skipMountHookNotifications(): Boolean = false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,65 @@ class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate {
std::move(runtimeExecutor)));
}

void addConsoleMessage(jsi::Runtime& /*unused*/, ConsoleMessage message)
override {
using HermesConsoleMessage = facebook::hermes::cdp::ConsoleMessage;
using HermesConsoleAPIType = facebook::hermes::cdp::ConsoleAPIType;

HermesConsoleAPIType type{};
switch (message.type) {
case ConsoleAPIType::kLog:
type = HermesConsoleAPIType::kLog;
break;
case ConsoleAPIType::kDebug:
type = HermesConsoleAPIType::kDebug;
break;
case ConsoleAPIType::kInfo:
type = HermesConsoleAPIType::kInfo;
break;
case ConsoleAPIType::kError:
type = HermesConsoleAPIType::kError;
break;
case ConsoleAPIType::kWarning:
type = HermesConsoleAPIType::kWarning;
break;
case ConsoleAPIType::kDir:
type = HermesConsoleAPIType::kDir;
break;
case ConsoleAPIType::kDirXML:
type = HermesConsoleAPIType::kDirXML;
break;
case ConsoleAPIType::kTable:
type = HermesConsoleAPIType::kTable;
break;
case ConsoleAPIType::kTrace:
type = HermesConsoleAPIType::kTrace;
break;
case ConsoleAPIType::kStartGroup:
type = HermesConsoleAPIType::kStartGroup;
break;
case ConsoleAPIType::kStartGroupCollapsed:
type = HermesConsoleAPIType::kStartGroupCollapsed;
break;
case ConsoleAPIType::kEndGroup:
type = HermesConsoleAPIType::kEndGroup;
break;
case ConsoleAPIType::kClear:
type = HermesConsoleAPIType::kClear;
break;
case ConsoleAPIType::kAssert:
type = HermesConsoleAPIType::kAssert;
break;
case ConsoleAPIType::kTimeEnd:
type = HermesConsoleAPIType::kTimeEnd;
break;
default:
throw std::logic_error{"Unknown console message type"};
}
cdpDebugAPI_->addConsoleMessage(
HermesConsoleMessage{message.timestamp, type, std::move(message.args)});
}

private:
HermesRuntimeTargetDelegate& delegate_;
std::shared_ptr<HermesRuntime> runtime_;
Expand Down Expand Up @@ -118,6 +177,12 @@ HermesRuntimeTargetDelegate::createAgentDelegate(
std::move(runtimeExecutor));
}

void HermesRuntimeTargetDelegate::addConsoleMessage(
jsi::Runtime& runtime,
ConsoleMessage message) {
impl_->addConsoleMessage(runtime, std::move(message));
}

#ifdef HERMES_ENABLE_DEBUGGER
CDPDebugAPI& HermesRuntimeTargetDelegate::getCDPDebugAPI() {
return impl_->getCDPDebugAPI();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ class HermesRuntimeTargetDelegate : public RuntimeTargetDelegate {
executionContextDescription,
RuntimeExecutor runtimeExecutor) override;

void addConsoleMessage(jsi::Runtime& runtime, ConsoleMessage message)
override;

private:
// We use the private implementation idiom to ensure this class has the same
// layout regardless of whether HERMES_ENABLE_DEBUGGER is defined. The net
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 <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
};

struct ConsoleMessage {
double timestamp;
ConsoleAPIType type;
std::vector<jsi::Value> args;

ConsoleMessage(
double timestamp,
ConsoleAPIType type,
std::vector<jsi::Value> args)
: timestamp(timestamp), type(type), args(std::move(args)) {}

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
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@ FallbackRuntimeTargetDelegate::createAgentDelegate(
std::move(channel), sessionState, engineDescription_);
}

void FallbackRuntimeTargetDelegate::addConsoleMessage(
jsi::Runtime& /*unused*/,
ConsoleMessage /*unused*/) {
// TODO: Best-effort printing (without RemoteObjects)
}

} // namespace facebook::react::jsinspector_modern
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class FallbackRuntimeTargetDelegate : public RuntimeTargetDelegate {
const ExecutionContextDescription& executionContextDescription,
RuntimeExecutor runtimeExecutor) override;

void addConsoleMessage(jsi::Runtime& runtime, ConsoleMessage message)
override;

private:
std::string engineDescription_;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ std::shared_ptr<RuntimeTarget> RuntimeTarget::create(
std::shared_ptr<RuntimeTarget> runtimeTarget{
new RuntimeTarget(executionContextDescription, delegate, jsExecutor)};
runtimeTarget->setExecutor(selfExecutor);
runtimeTarget->installGlobals();
return runtimeTarget;
}

Expand All @@ -32,6 +33,78 @@ RuntimeTarget::RuntimeTarget(
delegate_(delegate),
jsExecutor_(jsExecutor) {}

void RuntimeTarget::installGlobals() {
installConsoleHandler();
}

void RuntimeTarget::installConsoleHandler() {
jsExecutor_([selfWeak = weak_from_this(),
selfExecutor = executorFromThis()](jsi::Runtime& runtime) {
// TODO(moti): Switch from implementing __inspectorLog to directly
// installing a `console` object.
runtime.global().setProperty(
runtime,
"__inspectorLog",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "__inspectorLog"),
4,
[selfWeak, selfExecutor](
jsi::Runtime& rt,
const jsi::Value& /*thisVal*/,
const jsi::Value* args,
size_t count) {
if (count < 4) {
throw jsi::JSError(
rt,
"__inspectorLog requires at least 4 arguments: logLevel, str, args, framesToSkip");
}
std::chrono::time_point<std::chrono::system_clock> timestamp =
std::chrono::system_clock::now();
std::string level = args[0].asString(rt).utf8(rt);
ConsoleAPIType type = ConsoleAPIType::kLog;
if (level == "debug") {
type = ConsoleAPIType::kDebug;
} else if (level == "log") {
type = ConsoleAPIType::kLog;
} else if (level == "warning") {
type = ConsoleAPIType::kWarning;
} else if (level == "error") {
type = ConsoleAPIType::kError;
}
// NOTE: args[1] is the processed string message - ignore it.
jsi::Array argsArray = args[2].asObject(rt).asArray(rt);
std::vector<jsi::Value> argsVec;
for (size_t i = 0, length = argsArray.length(rt); i != length;
++i) {
argsVec.emplace_back(argsArray.getValueAtIndex(rt, i));
}
// TODO(moti): Handle framesToSkip in some way. Note that the
// runtime doesn't even capture a stack trace at the moment.
ConsoleMessage consoleMessage{
std::chrono::duration_cast<
std::chrono::duration<double, std::milli>>(
timestamp.time_since_epoch())
.count(),
type,
std::move(argsVec)};
if (auto self = selfWeak.lock()) {
// 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.
self->delegate_.addConsoleMessage(
rt, std::move(consoleMessage));
// To ensure we never destroy `self` on the JS thread, send
// our shared_ptr back to the inspector thread.
selfExecutor([self = std::move(self)](auto&) { (void)self; });
}
return jsi::Value::undefined();
}));
});
}

std::shared_ptr<RuntimeAgent> RuntimeTarget::createAgent(
FrontendChannel channel,
SessionState& sessionState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <ReactCommon/RuntimeExecutor.h>

#include "ConsoleMessage.h"
#include "ExecutionContext.h"
#include "InspectorInterfaces.h"
#include "RuntimeAgent.h"
Expand Down Expand Up @@ -51,6 +52,21 @@ class RuntimeTargetDelegate {
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;
};

/**
Expand Down Expand Up @@ -156,6 +172,17 @@ class JSINSPECTOR_EXPORT RuntimeTarget
*/
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();

// 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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ class MockRuntimeTargetDelegate : public RuntimeTargetDelegate {
const ExecutionContextDescription&,
RuntimeExecutor),
(override));
MOCK_METHOD(
void,
addConsoleMessage,
(jsi::Runtime & runtime, ConsoleMessage message),
(override));
};

class MockRuntimeAgentDelegate : public RuntimeAgentDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ void ReactInstanceIntegrationTest::initializeRuntime(std::string_view script) {
react::ReactInstance::JSRuntimeFlags flags{
.isProfiling = false,
};
instance->initializeRuntime(flags, [](jsi::Runtime&) {});
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();

Expand Down Expand Up @@ -199,11 +203,10 @@ TEST_P(ReactInstanceIntegrationTestWithFlags, ConsoleLog) {

InSequence s;

// Hermes console.* interception is currently explicitly disabled under the
// modern registry, and the runtime does not yet fire these events. When the
// implementation is more complete we should be able to remove this
// condition.
if (!InspectorFlags::getInstance().getEnableModernCDPRegistry()) {
// Hermes console.* interception is explicitly disabled under CDPHandler,
// but Hermes CDPAgent and the legacy RN backend should both work.
if (!InspectorFlags::getInstance().getEnableModernCDPRegistry() ||
InspectorFlags::getInstance().getEnableHermesCDPAgent()) {
EXPECT_CALL(
getRemoteConnection(),
onMessage(JsonParsed(AllOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<c63cd0b38dfa9c4d6843a4b879f8f4df>>
* @generated SignedSource<<c1c58e483f6f6f0a16d615f7c2168b4b>>
*/

/**
Expand Down Expand Up @@ -64,11 +64,11 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider {
}

bool inspectorEnableHermesCDPAgent() override {
return false;
return true;
}

bool inspectorEnableModernCDPRegistry() override {
return false;
return true;
}

bool skipMountHookNotifications() override {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ const definitions: FeatureFlagDefinitions = {
'Flag determining if the C++ implementation of InspectorPackagerConnection should be used instead of the per-platform one. This flag is global and should not be changed across React Host lifetimes.',
},
inspectorEnableHermesCDPAgent: {
defaultValue: false,
defaultValue: true,
description:
'Flag determining if the new Hermes CDPAgent API should be enabled in the modern CDP backend. This flag is global and should not be changed across React Host lifetimes.',
},
inspectorEnableModernCDPRegistry: {
defaultValue: false,
defaultValue: true,
description:
'Flag determining if the modern CDP backend should be enabled. This flag is global and should not be changed across React Host lifetimes.',
},
Expand Down
Loading