From 519a693e9e01a7f5eea849466c5fc7ac7e3c7f26 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Fri, 7 Nov 2025 08:23:27 -0800 Subject: [PATCH 1/2] Add single host inspector state assertion over CDP (#54407) Summary: Introduces a new `InspectorSystemState` object and `ReactNativeApplication.systemStateChanged` CDP event, used to assert whether more than one React Native Host has been registered for the current app lifetime. This will be used to disable the Performance and Network features in React Native DevTools when the debugger backend is in this currently unsupported state. We intend to implement host lifecycle correctness across all features soon. Changelog: [Internal] Differential Revision: D86201689 --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 ++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 +++- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../jsinspector-modern/HostAgent.cpp | 22 ++++++ .../jsinspector-modern/HostAgent.h | 6 ++ .../jsinspector-modern/HostTarget.h | 5 ++ .../jsinspector-modern/InspectorFlags.cpp | 6 ++ .../jsinspector-modern/InspectorFlags.h | 7 ++ .../InspectorInterfaces.cpp | 37 ++++++++++ .../jsinspector-modern/InspectorInterfaces.h | 13 +++- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 74 ++++++++++++------- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 ++- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactNativeFeatureFlags.config.js | 10 +++ .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 27 files changed, 258 insertions(+), 48 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 29bf8fb06bdd..9044ba47c6d6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -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<<9646ebeba75ec903be5ade7e2333f0c8>> + * @generated SignedSource<<248d4e5e599c3af43dc352c8bb1a2c13>> */ /** @@ -360,6 +360,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = accessor.fixMappingOfEventPrioritiesBetweenFabricAndReact() + /** + * Enable system assertion validating that Fusebox is configured with a single host. When set, the CDP backend will dynamically disable features (Perf and Network) in the event that multiple hosts are registered (undefined behaviour), and broadcast this over `ReactNativeApplication.systemStateChanged`. + */ + @JvmStatic + public fun fuseboxAssertSingleHostState(): Boolean = accessor.fuseboxAssertSingleHostState() + /** * Flag determining if the React Native DevTools (Fusebox) CDP backend should be enabled in release builds. This flag is global and should not be changed across React Host lifetimes. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index ec42740a8754..fd4e49d5ff9d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -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<<9d6ccbe6d02608901fc18ad88baab176>> + * @generated SignedSource<> */ /** @@ -75,6 +75,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableVirtualViewWindowFocusDetectionCache: Boolean? = null private var enableWebPerformanceAPIsByDefaultCache: Boolean? = null private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null + private var fuseboxAssertSingleHostStateCache: Boolean? = null private var fuseboxEnabledReleaseCache: Boolean? = null private var fuseboxNetworkInspectionEnabledCache: Boolean? = null private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null @@ -598,6 +599,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun fuseboxAssertSingleHostState(): Boolean { + var cached = fuseboxAssertSingleHostStateCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.fuseboxAssertSingleHostState() + fuseboxAssertSingleHostStateCache = cached + } + return cached + } + override fun fuseboxEnabledRelease(): Boolean { var cached = fuseboxEnabledReleaseCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index d49354281fdd..e09b5830fda0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -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<> + * @generated SignedSource<> */ /** @@ -138,6 +138,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean + @DoNotStrip @JvmStatic public external fun fuseboxAssertSingleHostState(): Boolean + @DoNotStrip @JvmStatic public external fun fuseboxEnabledRelease(): Boolean @DoNotStrip @JvmStatic public external fun fuseboxNetworkInspectionEnabled(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index fdd3b815149e..42bd104b71cc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -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<<7b8a5ad9a3353ea32a39bd139e9174f7>> + * @generated SignedSource<<30041179508b76146b4a6ff70f7d2090>> */ /** @@ -133,6 +133,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = false + override fun fuseboxAssertSingleHostState(): Boolean = true + override fun fuseboxEnabledRelease(): Boolean = false override fun fuseboxNetworkInspectionEnabled(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index ec66c76e0c87..085d26c6e576 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -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<<9bb9a7cf89c92f5a397b2328fa983dc6>> + * @generated SignedSource<<78f60952fc7a5cd0efbf8ba8478251a7>> */ /** @@ -79,6 +79,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableVirtualViewWindowFocusDetectionCache: Boolean? = null private var enableWebPerformanceAPIsByDefaultCache: Boolean? = null private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null + private var fuseboxAssertSingleHostStateCache: Boolean? = null private var fuseboxEnabledReleaseCache: Boolean? = null private var fuseboxNetworkInspectionEnabledCache: Boolean? = null private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null @@ -657,6 +658,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun fuseboxAssertSingleHostState(): Boolean { + var cached = fuseboxAssertSingleHostStateCache + if (cached == null) { + cached = currentProvider.fuseboxAssertSingleHostState() + accessedFeatureFlags.add("fuseboxAssertSingleHostState") + fuseboxAssertSingleHostStateCache = cached + } + return cached + } + override fun fuseboxEnabledRelease(): Boolean { var cached = fuseboxEnabledReleaseCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index cd76bc3df19c..1c6a0883a6d3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -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<> + * @generated SignedSource<<865a11fa77d36e33d3bb6548cce01e66>> */ /** @@ -133,6 +133,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean + @DoNotStrip public fun fuseboxAssertSingleHostState(): Boolean + @DoNotStrip public fun fuseboxEnabledRelease(): Boolean @DoNotStrip public fun fuseboxNetworkInspectionEnabled(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index df06cc588264..944f0319d208 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -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<<0527dbb4a838be34b80d76b11d18cea0>> + * @generated SignedSource<<975dfce969af2eda791cd6dc37d6ccc2>> */ /** @@ -369,6 +369,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool fuseboxAssertSingleHostState() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fuseboxAssertSingleHostState"); + return method(javaProvider_); + } + bool fuseboxEnabledRelease() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fuseboxEnabledRelease"); @@ -810,6 +816,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabri return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool JReactNativeFeatureFlagsCxxInterop::fuseboxAssertSingleHostState( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::fuseboxAssertSingleHostState(); +} + bool JReactNativeFeatureFlagsCxxInterop::fuseboxEnabledRelease( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::fuseboxEnabledRelease(); @@ -1141,6 +1152,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "fixMappingOfEventPrioritiesBetweenFabricAndReact", JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabricAndReact), + makeNativeMethod( + "fuseboxAssertSingleHostState", + JReactNativeFeatureFlagsCxxInterop::fuseboxAssertSingleHostState), makeNativeMethod( "fuseboxEnabledRelease", JReactNativeFeatureFlagsCxxInterop::fuseboxEnabledRelease), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 64a4b5ec585d..5b66f4dcf5b2 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -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<> + * @generated SignedSource<<97c126a1763860d25ee216150156e200>> */ /** @@ -195,6 +195,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool fixMappingOfEventPrioritiesBetweenFabricAndReact( facebook::jni::alias_ref); + static bool fuseboxAssertSingleHostState( + facebook::jni::alias_ref); + static bool fuseboxEnabledRelease( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 60a33116bd15..9464d625695b 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp @@ -10,6 +10,7 @@ #ifdef REACT_NATIVE_DEBUGGER_ENABLED #include "InspectorFlags.h" +#include "InspectorInterfaces.h" #include "NetworkIOAgent.h" #include "SessionState.h" #include "TracingAgent.h" @@ -216,6 +217,11 @@ class HostAgent::Impl final { cdp::jsonNotification( "ReactNativeApplication.metadataUpdated", createHostMetadataPayload(hostMetadata_))); + auto& inspector = getInspectorInstance(); + bool isSingleHost = inspector.getSystemState().registeredPagesCount <= 1; + if (!isSingleHost) { + emitSystemStateChanged(isSingleHost); + } auto stashedTraceRecording = targetController_.getDelegate() @@ -374,6 +380,17 @@ class HostAgent::Impl final { tracingAgent_.emitExternalTraceRecording(std::move(traceRecording)); } + void emitSystemStateChanged(bool isSingleHost) { + frontendChannel_( + cdp::jsonNotification( + "ReactNativeApplication.systemStateChanged", + folly::dynamic::object("isSingleHost", isSingleHost))); + + if (!isSingleHost) { + frontendChannel_(cdp::jsonNotification("Network.disable")); + } + } + private: enum class FuseboxClientType { Unknown, Fusebox, NonFusebox }; @@ -480,6 +497,7 @@ class HostAgent::Impl final { } void emitExternalTraceRecording(tracing::TraceRecordingState traceRecording) { } + void emitSystemStateChanged(bool isSingleHost) {} }; #endif // REACT_NATIVE_DEBUGGER_ENABLED @@ -519,6 +537,10 @@ void HostAgent::emitExternalTraceRecording( impl_->emitExternalTraceRecording(std::move(traceRecording)); } +void HostAgent::emitSystemStateChanged(bool isSingleHost) const { + impl_->emitSystemStateChanged(isSingleHost); +} + #pragma mark - Tracing HostTracingAgent::HostTracingAgent(tracing::TraceRecordingState& state) diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h index afb8bf44ad40..ab4478816562 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h @@ -79,6 +79,12 @@ class HostAgent final { */ 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 diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h index e315893e8e11..72929a95a205 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h @@ -326,6 +326,11 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis */ 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. diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp index 35092ec4e6f5..010813a24264 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp @@ -17,6 +17,10 @@ InspectorFlags& InspectorFlags::getInstance() { return instance; } +bool InspectorFlags::getAssertSingleHostState() const { + return loadFlagsAndAssertUnchanged().assertSingleHostState; +} + bool InspectorFlags::getFuseboxEnabled() const { if (fuseboxDisabledForTest_) { return false; @@ -48,6 +52,8 @@ void InspectorFlags::dangerouslyDisableFuseboxForTest() { const InspectorFlags::Values& InspectorFlags::loadFlagsAndAssertUnchanged() const { InspectorFlags::Values newValues = { + .assertSingleHostState = + ReactNativeFeatureFlags::fuseboxAssertSingleHostState(), .fuseboxEnabled = #if defined(REACT_NATIVE_DEBUGGER_ENABLED) true, diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h index 60e260ea19bf..5e245d482eed 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h @@ -19,6 +19,12 @@ 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. */ @@ -54,6 +60,7 @@ class InspectorFlags { private: struct Values { + bool assertSingleHostState; bool fuseboxEnabled; bool isProfilingBuild; bool networkInspectionEnabled; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.cpp index d660504a84d7..b0d2f7a5ffd4 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.cpp @@ -7,6 +7,8 @@ #include "InspectorInterfaces.h" +#include "InspectorFlags.h" + #include #include #include @@ -35,6 +37,14 @@ namespace { class InspectorImpl : public IInspector { public: + InspectorImpl() { + systemStateListener_ = std::make_shared(systemState_); + auto& inspectorFlags = InspectorFlags::getInstance(); + if (inspectorFlags.getAssertSingleHostState()) { + registerPageStatusListener(systemStateListener_); + } + } + int addPage( const std::string& description, const std::string& vm, @@ -50,7 +60,21 @@ class InspectorImpl : public IInspector { void registerPageStatusListener( std::weak_ptr listener) override; + InspectorSystemState getSystemState() const override; + private: + class SystemStateListener : public IPageStatusListener { + public: + explicit SystemStateListener(InspectorSystemState& state) : state_(state) {} + + void onPageAdded(int /*pageId*/) override { + state_.registeredPagesCount++; + } + + private: + InspectorSystemState& state_; + }; + class Page { public: Page( @@ -74,6 +98,8 @@ class InspectorImpl : public IInspector { int nextPageId_{1}; std::map pages_; std::list> listeners_; + InspectorSystemState systemState_{0}; + std::shared_ptr systemStateListener_; }; InspectorImpl::Page::Page( @@ -116,6 +142,12 @@ int InspectorImpl::addPage( pageId, Page{pageId, description, vm, std::move(connectFunc), capabilities}); + for (const auto& listenerWeak : listeners_) { + if (auto listener = listenerWeak.lock()) { + listener->onPageAdded(pageId); + } + } + return pageId; } @@ -175,6 +207,11 @@ void InspectorImpl::registerPageStatusListener( } listeners_.push_back(listener); } + +InspectorSystemState InspectorImpl::getSystemState() const { + std::scoped_lock lock(mutex_); + return systemState_; +} } // namespace IInspector& getInspectorInstance() { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.h b/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.h index 04e45131e5e7..3117b216ac2d 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.h @@ -51,6 +51,11 @@ struct InspectorPageDescription { // Alias for backwards compatibility. using InspectorPage = InspectorPageDescription; +struct InspectorSystemState { + /** The total count of pages registered during the app lifetime. */ + int registeredPagesCount; +}; + /// 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. @@ -78,7 +83,8 @@ class JSINSPECTOR_EXPORT ILocalConnection : public IDestructible { class JSINSPECTOR_EXPORT IPageStatusListener : public IDestructible { public: virtual ~IPageStatusListener() = 0; - virtual void onPageRemoved(int pageId) = 0; + virtual void onPageAdded(int /*pageId*/) {} + virtual void onPageRemoved(int /*pageId*/) {} }; /// IInspector tracks debuggable JavaScript targets (pages). @@ -127,6 +133,11 @@ class JSINSPECTOR_EXPORT IInspector : public IDestructible { * when pages are removed. */ virtual void registerPageStatusListener(std::weak_ptr listener) = 0; + + /** + * Get the current \c InspectorSystemState object. + */ + virtual InspectorSystemState getSystemState() const = 0; }; class NotImplementedException : public std::exception { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index c3adaa661754..eab3bf4e14f3 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -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<> + * @generated SignedSource<> */ /** @@ -246,6 +246,10 @@ bool ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact() return getAccessor().fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool ReactNativeFeatureFlags::fuseboxAssertSingleHostState() { + return getAccessor().fuseboxAssertSingleHostState(); +} + bool ReactNativeFeatureFlags::fuseboxEnabledRelease() { return getAccessor().fuseboxEnabledRelease(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 5a46288842e9..b4c4eadcdaa9 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -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<<467f48f2231ceb6772a9a9da9e3badb9>> + * @generated SignedSource<<6fdcfc50aba88a432b35083ab96c3844>> */ /** @@ -314,6 +314,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); + /** + * Enable system assertion validating that Fusebox is configured with a single host. When set, the CDP backend will dynamically disable features (Perf and Network) in the event that multiple hosts are registered (undefined behaviour), and broadcast this over `ReactNativeApplication.systemStateChanged`. + */ + RN_EXPORT static bool fuseboxAssertSingleHostState(); + /** * Flag determining if the React Native DevTools (Fusebox) CDP backend should be enabled in release builds. This flag is global and should not be changed across React Host lifetimes. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 7093ce280a74..0da7a8c891b0 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -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<<8f6640b5dc86a3f50b14ba9d222de89c>> + * @generated SignedSource<<2c76b98618bad8b17782632dc0fa1b46>> */ /** @@ -1019,6 +1019,24 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { + auto flagValue = fuseboxAssertSingleHostState_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(55, "fuseboxAssertSingleHostState"); + + flagValue = currentProvider_->fuseboxAssertSingleHostState(); + fuseboxAssertSingleHostState_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { auto flagValue = fuseboxEnabledRelease_.load(); @@ -1028,7 +1046,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(55, "fuseboxEnabledRelease"); + markFlagAsAccessed(56, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1046,7 +1064,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(56, "fuseboxNetworkInspectionEnabled"); + markFlagAsAccessed(57, "fuseboxNetworkInspectionEnabled"); flagValue = currentProvider_->fuseboxNetworkInspectionEnabled(); fuseboxNetworkInspectionEnabled_ = flagValue; @@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::hideOffscreenVirtualViewsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(57, "hideOffscreenVirtualViewsOnIOS"); + markFlagAsAccessed(58, "hideOffscreenVirtualViewsOnIOS"); flagValue = currentProvider_->hideOffscreenVirtualViewsOnIOS(); hideOffscreenVirtualViewsOnIOS_ = flagValue; @@ -1082,7 +1100,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(58, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(59, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1100,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(59, "perfIssuesEnabled"); + markFlagAsAccessed(60, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1118,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "perfMonitorV2Enabled"); + markFlagAsAccessed(61, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1136,7 +1154,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "preparedTextCacheSize"); + markFlagAsAccessed(62, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(63, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(64, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(65, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(66, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(67, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(68, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(69, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "useFabricInterop"); + markFlagAsAccessed(70, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeEqualsInNativeReadableArrayAndroi // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "useNativeEqualsInNativeReadableArrayAndroid"); + markFlagAsAccessed(71, "useNativeEqualsInNativeReadableArrayAndroid"); flagValue = currentProvider_->useNativeEqualsInNativeReadableArrayAndroid(); useNativeEqualsInNativeReadableArrayAndroid_ = flagValue; @@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeTransformHelperAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "useNativeTransformHelperAndroid"); + markFlagAsAccessed(72, "useNativeTransformHelperAndroid"); flagValue = currentProvider_->useNativeTransformHelperAndroid(); useNativeTransformHelperAndroid_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(73, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1352,7 +1370,7 @@ bool ReactNativeFeatureFlagsAccessor::useRawPropsJsiValue() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "useRawPropsJsiValue"); + markFlagAsAccessed(74, "useRawPropsJsiValue"); flagValue = currentProvider_->useRawPropsJsiValue(); useRawPropsJsiValue_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::useShadowNodeStateOnClone() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "useShadowNodeStateOnClone"); + markFlagAsAccessed(75, "useShadowNodeStateOnClone"); flagValue = currentProvider_->useShadowNodeStateOnClone(); useShadowNodeStateOnClone_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "useSharedAnimatedBackend"); + markFlagAsAccessed(76, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(77, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "useTurboModuleInterop"); + markFlagAsAccessed(78, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "useTurboModules"); + markFlagAsAccessed(79, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1460,7 +1478,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "viewCullingOutsetRatio"); + markFlagAsAccessed(80, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1478,7 +1496,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewHysteresisRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "virtualViewHysteresisRatio"); + markFlagAsAccessed(81, "virtualViewHysteresisRatio"); flagValue = currentProvider_->virtualViewHysteresisRatio(); virtualViewHysteresisRatio_ = flagValue; @@ -1496,7 +1514,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "virtualViewPrerenderRatio"); + markFlagAsAccessed(82, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index b626cc49c70c..35e35c0efaeb 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -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<> + * @generated SignedSource<> */ /** @@ -87,6 +87,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableVirtualViewWindowFocusDetection(); bool enableWebPerformanceAPIsByDefault(); bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); + bool fuseboxAssertSingleHostState(); bool fuseboxEnabledRelease(); bool fuseboxNetworkInspectionEnabled(); bool hideOffscreenVirtualViewsOnIOS(); @@ -125,7 +126,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 82> accessedFeatureFlags_; + std::array, 83> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -182,6 +183,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableVirtualViewWindowFocusDetection_; std::atomic> enableWebPerformanceAPIsByDefault_; std::atomic> fixMappingOfEventPrioritiesBetweenFabricAndReact_; + std::atomic> fuseboxAssertSingleHostState_; std::atomic> fuseboxEnabledRelease_; std::atomic> fuseboxNetworkInspectionEnabled_; std::atomic> hideOffscreenVirtualViewsOnIOS_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 32aecdc212e3..56364605fe2a 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -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<<85c260dcb8eb9209a53207c3c54183f0>> + * @generated SignedSource<> */ /** @@ -247,6 +247,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool fuseboxAssertSingleHostState() override { + return true; + } + bool fuseboxEnabledRelease() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 34ce8c68d0c7..a0a9bb21a870 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -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<<0224a638fbec556a62c58f5d84c4c662>> + * @generated SignedSource<> */ /** @@ -540,6 +540,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } + bool fuseboxAssertSingleHostState() override { + auto value = values_["fuseboxAssertSingleHostState"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fuseboxAssertSingleHostState(); + } + bool fuseboxEnabledRelease() override { auto value = values_["fuseboxEnabledRelease"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index f2dcc71af4ec..3105f8e2bacf 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -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<> + * @generated SignedSource<> */ /** @@ -80,6 +80,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableVirtualViewWindowFocusDetection() = 0; virtual bool enableWebPerformanceAPIsByDefault() = 0; virtual bool fixMappingOfEventPrioritiesBetweenFabricAndReact() = 0; + virtual bool fuseboxAssertSingleHostState() = 0; virtual bool fuseboxEnabledRelease() = 0; virtual bool fuseboxNetworkInspectionEnabled() = 0; virtual bool hideOffscreenVirtualViewsOnIOS() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 05c867b7f714..3599dac5daf3 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -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<> + * @generated SignedSource<> */ /** @@ -319,6 +319,11 @@ bool NativeReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndR return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool NativeReactNativeFeatureFlags::fuseboxAssertSingleHostState( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::fuseboxAssertSingleHostState(); +} + bool NativeReactNativeFeatureFlags::fuseboxEnabledRelease( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::fuseboxEnabledRelease(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index cd406e004de4..6b4e0a48dadd 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -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<<0d898a94decb43e191a343676c8afb91>> + * @generated SignedSource<> */ /** @@ -146,6 +146,8 @@ class NativeReactNativeFeatureFlags bool fixMappingOfEventPrioritiesBetweenFabricAndReact(jsi::Runtime& runtime); + bool fuseboxAssertSingleHostState(jsi::Runtime& runtime); + bool fuseboxEnabledRelease(jsi::Runtime& runtime); bool fuseboxNetworkInspectionEnabled(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 6388b73719a7..e875eb561b89 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -634,6 +634,16 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + fuseboxAssertSingleHostState: { + defaultValue: true, + metadata: { + description: + 'Enable system assertion validating that Fusebox is configured with a single host. When set, the CDP backend will dynamically disable features (Perf and Network) in the event that multiple hosts are registered (undefined behaviour), and broadcast this over `ReactNativeApplication.systemStateChanged`.', + expectedReleaseValue: true, + purpose: 'operational', + }, + ossReleaseStage: 'stable', + }, fuseboxEnabledRelease: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index aa7c52f4a1b2..3334c3766952 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -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<> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -105,6 +105,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ enableVirtualViewWindowFocusDetection: Getter, enableWebPerformanceAPIsByDefault: Getter, fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter, + fuseboxAssertSingleHostState: Getter, fuseboxEnabledRelease: Getter, fuseboxNetworkInspectionEnabled: Getter, hideOffscreenVirtualViewsOnIOS: Getter, @@ -433,6 +434,10 @@ export const enableWebPerformanceAPIsByDefault: Getter = createNativeFl * Uses the default event priority instead of the discreet event priority by default when dispatching events from Fabric to React. */ export const fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter = createNativeFlagGetter('fixMappingOfEventPrioritiesBetweenFabricAndReact', false); +/** + * Enable system assertion validating that Fusebox is configured with a single host. When set, the CDP backend will dynamically disable features (Perf and Network) in the event that multiple hosts are registered (undefined behaviour), and broadcast this over `ReactNativeApplication.systemStateChanged`. + */ +export const fuseboxAssertSingleHostState: Getter = createNativeFlagGetter('fuseboxAssertSingleHostState', true); /** * Flag determining if the React Native DevTools (Fusebox) CDP backend should be enabled in release builds. This flag is global and should not be changed across React Host lifetimes. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 218586e3e244..8858f48afdf6 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -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<<53fc9aca82f76eb0519b03187993359a>> + * @generated SignedSource<<0bd1151b93fcc3a7ab6572f48d31b3cf>> * @flow strict * @noformat */ @@ -80,6 +80,7 @@ export interface Spec extends TurboModule { +enableVirtualViewWindowFocusDetection?: () => boolean; +enableWebPerformanceAPIsByDefault?: () => boolean; +fixMappingOfEventPrioritiesBetweenFabricAndReact?: () => boolean; + +fuseboxAssertSingleHostState?: () => boolean; +fuseboxEnabledRelease?: () => boolean; +fuseboxNetworkInspectionEnabled?: () => boolean; +hideOffscreenVirtualViewsOnIOS?: () => boolean; From 99a382ac0cfa6f8c4a7cacfa12392946f1af31e5 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Fri, 7 Nov 2025 08:23:27 -0800 Subject: [PATCH 2/2] Update Network and Tracing handlers to error under multiple hosts (#54408) Summary: Follows from the parent commit. This will prevent 3P CDP clients from using the `Tracing` or `Network` domains when we are in an unsupported multi React Native Host setup. Changelog: [Internal] Reviewed By: hoxyq Differential Revision: D86201688 --- .../jsinspector-modern/HostAgent.cpp | 17 ++++++++++++++--- .../jsinspector-modern/TracingAgent.cpp | 11 +++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 9464d625695b..813770d51ad5 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp @@ -146,6 +146,19 @@ class HostAgent::Impl final { } if (InspectorFlags::getInstance().getNetworkInspectionEnabled()) { if (req.method == "Network.enable") { + auto& inspector = getInspectorInstance(); + if (inspector.getSystemState().registeredPagesCount > 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 { @@ -386,9 +399,7 @@ class HostAgent::Impl final { "ReactNativeApplication.systemStateChanged", folly::dynamic::object("isSingleHost", isSingleHost))); - if (!isSingleHost) { - frontendChannel_(cdp::jsonNotification("Network.disable")); - } + frontendChannel_(cdp::jsonNotification("Network.disable")); } private: diff --git a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp index ee4942bc02c5..9ed49254f63d 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp @@ -49,6 +49,17 @@ TracingAgent::~TracingAgent() { bool TracingAgent::handleRequest(const cdp::PreparsedRequest& req) { if (req.method == "Tracing.start") { + auto& inspector = getInspectorInstance(); + if (inspector.getSystemState().registeredPagesCount > 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(