From 23db8efc729fda00fd5be2969111a316ac91ee38 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Mon, 29 Apr 2024 11:38:27 -0400 Subject: [PATCH 1/9] Adds device parameter to InspectorPackagerConnection URL In preparation for unblocking Fusebox in react-native-windows, this change adds a stable device ID, which is a hash of the publisher device ID from Windows APIs and the bundle ID. --- .../ReactHost/ReactInstanceWin.cpp | 2 +- vnext/Shared/DevServerHelper.h | 8 ++- vnext/Shared/DevSupportManager.cpp | 55 ++++++++++++++++--- vnext/Shared/DevSupportManager.h | 5 +- vnext/Shared/IDevSupportManager.h | 5 +- vnext/Shared/OInstance.cpp | 38 +++++++------ 6 files changed, 80 insertions(+), 33 deletions(-) diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp index 379b1b2e2f1..5251bedaafb 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp @@ -611,7 +611,7 @@ void ReactInstanceWin::InitializeBridgeless() noexcept { if (devSettings->useDirectDebugger) { ::Microsoft::ReactNative::GetSharedDevManager()->EnsureHermesInspector( - devSettings->sourceBundleHost, devSettings->sourceBundlePort); + devSettings->sourceBundleHost, devSettings->sourceBundlePort, devSettings->bundleAppId); } m_jsiRuntimeHolder = std::make_shared( diff --git a/vnext/Shared/DevServerHelper.h b/vnext/Shared/DevServerHelper.h index 384a7619810..67587e2b521 100644 --- a/vnext/Shared/DevServerHelper.h +++ b/vnext/Shared/DevServerHelper.h @@ -76,12 +76,14 @@ class DevServerHelper { const std::string &packagerHost, const uint16_t packagerPort, const std::string &deviceName, - const std::string &packageName) { + const std::string &packageName, + const std::string &deviceId) { return string_format( InspectorDeviceUrlFormat, GetDeviceLocalHost(packagerHost, packagerPort).c_str(), deviceName.c_str(), - packageName.c_str()); + packageName.c_str(), + deviceId.c_str()); } static constexpr const char DefaultPackagerHost[] = "localhost"; @@ -105,7 +107,7 @@ class DevServerHelper { static constexpr const char PackagerConnectionUrlFormat[] = "ws://%s/message"; static constexpr const char PackagerStatusUrlFormat[] = "http://%s/status"; static constexpr const char PackagerOpenStackFrameUrlFormat[] = "https://%s/open-stack-frame"; - static constexpr const char InspectorDeviceUrlFormat[] = "ws://%s/inspector/device?name=%s&app=%s"; + static constexpr const char InspectorDeviceUrlFormat[] = "ws://%s/inspector/device?name=%s&app=%s&device=%s"; static constexpr const char PackagerOkStatus[] = "packager-status:running"; const int LongPollFailureDelayMs = 5000; diff --git a/vnext/Shared/DevSupportManager.cpp b/vnext/Shared/DevSupportManager.cpp index a6935bb8aa9..29eb9f22c61 100644 --- a/vnext/Shared/DevSupportManager.cpp +++ b/vnext/Shared/DevSupportManager.cpp @@ -16,7 +16,10 @@ #include #include +#include +#include #include +#include #include #include #include @@ -171,6 +174,33 @@ bool IsIgnorablePollHResult(HRESULT hr) { return hr == WININET_E_INVALID_SERVER_RESPONSE; } +std::string GetDeviceId(const std::string &bundleAppId) { + const auto hash = winrt::Windows::Security::Cryptography::Core::HashAlgorithmProvider::OpenAlgorithm( + winrt::Windows::Security::Cryptography::Core::HashAlgorithmNames::Sha256()) + .CreateHash(); + hash.Append(winrt::Windows::System::Profile::SystemIdentification::GetSystemIdForPublisher().Id()); + winrt::Windows::Storage::Streams::InMemoryRandomAccessStream stream; + winrt::Windows::Storage::Streams::DataWriter writer; + // If an app ID is provided, we will allow reconnection to DevTools. + // Apps must supply a unique app ID to each ReactNativeHost instance settings for this to behave correctly. + if (!bundleAppId.empty()) { + const auto bundleAppIdBuffer = winrt::Windows::Security::Cryptography::CryptographicBuffer::ConvertStringToBinary( + winrt::to_hstring(bundleAppId), winrt::Windows::Security::Cryptography::BinaryStringEncoding::Utf16BE); + hash.Append(bundleAppIdBuffer); + } else { + const auto processId = GetCurrentProcessId(); + std::vector processIdBytes( + reinterpret_cast(&processId), reinterpret_cast(&processId + 1)); + winrt::array_view processIdByteArray(processIdBytes); + const auto processIdBuffer = + winrt::Windows::Security::Cryptography::CryptographicBuffer::CreateFromByteArray(processIdByteArray); + hash.Append(processIdBuffer); + } + const auto hashBuffer = hash.GetValueAndReset(); + const auto hashString = winrt::Windows::Security::Cryptography::CryptographicBuffer::EncodeToHexString(hashBuffer); + return winrt::to_string(hashString); +} + std::future PollForLiveReload(const std::string &url) { winrt::Windows::Web::Http::HttpClient httpClient; winrt::Windows::Foundation::Uri uri(Microsoft::Common::Unicode::Utf8ToUtf16(url)); @@ -240,16 +270,21 @@ void DevSupportManager::StopPollingLiveReload() { void DevSupportManager::EnsureHermesInspector( [[maybe_unused]] const std::string &packagerHost, - [[maybe_unused]] const uint16_t packagerPort) noexcept { + [[maybe_unused]] const uint16_t packagerPort, + [[maybe_unused]] const std::string &bundleAppId) noexcept { static std::once_flag once; - std::call_once(once, [this, &packagerHost, packagerPort]() { + std::call_once(once, [this, &packagerHost, packagerPort, &jsBundleName]() { // TODO: should we use the bundleAppId as the app param if available? - std::string packageName("RNW"); - wchar_t fullName[PACKAGE_FULL_NAME_MAX_LENGTH]{}; - UINT32 size = ARRAYSIZE(fullName); - if (SUCCEEDED(GetCurrentPackageFullName(&size, fullName))) { - // we are in an unpackaged app - packageName = winrt::to_string(fullName); + + std::string packageName{bundleAppId}; + if (packageName == "") { + std::string packageName("RNW"); + wchar_t fullName[PACKAGE_FULL_NAME_MAX_LENGTH]{}; + UINT32 size = ARRAYSIZE(fullName); + if (SUCCEEDED(GetCurrentPackageFullName(&size, fullName))) { + // we are in an unpackaged app + packageName = winrt::to_string(fullName); + } } std::string deviceName("RNWHost"); @@ -258,8 +293,10 @@ void DevSupportManager::EnsureHermesInspector( deviceName = winrt::to_string(hostNames.First().Current().DisplayName()); } + const auto deviceId = GetDeviceId(packageName); m_inspectorPackagerConnection = std::make_shared( - facebook::react::DevServerHelper::get_InspectorDeviceUrl(packagerHost, packagerPort, deviceName, packageName), + facebook::react::DevServerHelper::get_InspectorDeviceUrl( + packagerHost, packagerPort, deviceName, packageName, deviceId), m_BundleStatusProvider); m_inspectorPackagerConnection->connectAsync(); }); diff --git a/vnext/Shared/DevSupportManager.h b/vnext/Shared/DevSupportManager.h index 5c33ed1de02..6e25131baa3 100644 --- a/vnext/Shared/DevSupportManager.h +++ b/vnext/Shared/DevSupportManager.h @@ -49,7 +49,10 @@ class DevSupportManager final : public facebook::react::IDevSupportManager { std::function onChangeCallback) override; virtual void StopPollingLiveReload() override; - virtual void EnsureHermesInspector(const std::string &packagerHost, const uint16_t packagerPort) noexcept override; + virtual void EnsureHermesInspector( + const std::string &packagerHost, + const uint16_t packagerPort, + const std::string &bundleAppId) noexcept override; virtual void UpdateBundleStatus(bool isLastDownloadSuccess, int64_t updateTimestamp) noexcept override; private: diff --git a/vnext/Shared/IDevSupportManager.h b/vnext/Shared/IDevSupportManager.h index dd90ff1bfc7..4ebde10186e 100644 --- a/vnext/Shared/IDevSupportManager.h +++ b/vnext/Shared/IDevSupportManager.h @@ -24,7 +24,10 @@ struct IDevSupportManager { std::function onChangeCallback) = 0; virtual void StopPollingLiveReload() = 0; - virtual void EnsureHermesInspector(const std::string &packagerHost, const uint16_t packagerPort) noexcept = 0; + virtual void EnsureHermesInspector( + const std::string &packagerHost, + const uint16_t packagerPort, + const std::string &bundleAppId) noexcept = 0; virtual void UpdateBundleStatus(bool isLastDownloadSuccess, int64_t updateTimestamp) noexcept = 0; }; diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index ece2c869a9c..1ce98cf6534 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -73,6 +73,20 @@ using namespace Microsoft::JSI; using std::make_shared; using winrt::Microsoft::ReactNative::ReactPropertyBagHelper; +namespace facebook::react { +bool shouldStartHermesInspector(DevSettings &devSettings) { + bool isHermes = + ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || + (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && + devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); + + if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) + return true; + else + return false; +} +} // namespace facebook::react + namespace Microsoft::ReactNative { // Note: Based on @@ -105,6 +119,12 @@ void LoadRemoteUrlScript( hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; #endif + const auto bundlePath = ; + if (facebook::react::shouldStartHermesInspector(*devSettings)) { + devManager->EnsureHermesInspector( + devSettings->sourceBundleHost, devSettings->sourceBundlePort, devSettings->bundleAppId); + } + auto [jsBundleString, success] = GetJavaScriptFromServer( devSettings->sourceBundleHost, devSettings->sourceBundlePort, @@ -329,20 +349,6 @@ void InstanceImpl::SetInError() noexcept { m_isInError = true; } -namespace { -bool shouldStartHermesInspector(DevSettings &devSettings) { - bool isHermes = - ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || - (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && - devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); - - if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) - return true; - else - return false; -} -} // namespace - InstanceImpl::InstanceImpl( std::shared_ptr &&instance, std::string &&jsBundleBasePath, @@ -373,10 +379,6 @@ InstanceImpl::InstanceImpl( facebook::react::tracing::initializeETW(); #endif - if (shouldStartHermesInspector(*m_devSettings)) { - m_devManager->EnsureHermesInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); - } - // Default (common) NativeModules auto modules = GetDefaultNativeModules(nativeQueue); From 7d33a05b5e2bfddcdace6e64e5b487ff94d8b14a Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Wed, 1 May 2024 16:41:09 -0400 Subject: [PATCH 2/9] Add static thread pool queue for inspector Adds thread pool queue for Fusebox debugger for use when implementing callbacks for the HostTarget or the InspectorPackagerConnection. --- vnext/Shared/FuseboxInspectorThread.h | 18 ++++++++++++++++++ vnext/Shared/Shared.vcxitems | 1 + vnext/Shared/Shared.vcxitems.filters | 3 +++ 3 files changed, 22 insertions(+) create mode 100644 vnext/Shared/FuseboxInspectorThread.h diff --git a/vnext/Shared/FuseboxInspectorThread.h b/vnext/Shared/FuseboxInspectorThread.h new file mode 100644 index 00000000000..e33311410f5 --- /dev/null +++ b/vnext/Shared/FuseboxInspectorThread.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace Microsoft::ReactNative { + +class FuseboxInspectorThread final { + public: + static Mso::DispatchQueue &Instance() { + static Mso::DispatchQueue queue = Mso::DispatchQueue::MakeSerialQueue(); + return queue; + } +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index 47caa4d4318..ebc62c54cc8 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -412,6 +412,7 @@ + diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index babf9f193ac..1dd765e3906 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -507,6 +507,9 @@ Header Files + + Header Files + Header Files From 5358bfdba55a91deb885a55fb20ab92056c0c42b Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Mon, 29 Apr 2024 12:19:46 -0400 Subject: [PATCH 3/9] Add HostTarget to CDP registry Wires up the HostTarget in ReactNativeHost for react-native-windows. This code will only run if InspectorFlags::getEnableModernCDPRegistry() returns true. --- .../Microsoft.ReactNative.vcxproj | 1 + .../Microsoft.ReactNative.vcxproj.filters | 3 + .../ReactHost/DebuggerNotifications.h | 54 ++++++++++++ vnext/Microsoft.ReactNative/ReactHost/React.h | 7 ++ .../ReactHost/ReactInstanceWin.cpp | 22 ++++- .../ReactHost/ReactInstanceWin.h | 2 +- .../Microsoft.ReactNative/ReactNativeHost.cpp | 87 +++++++++++++++++++ vnext/Microsoft.ReactNative/ReactNativeHost.h | 7 ++ vnext/ReactCommon/ReactCommon.vcxproj | 2 + vnext/Shared/DevSettings.h | 7 ++ vnext/Shared/OInstance.cpp | 13 ++- 11 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 vnext/Microsoft.ReactNative/ReactHost/DebuggerNotifications.h diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj index 932722fbf5f..e2683d37b9a 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj @@ -281,6 +281,7 @@ Code + diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters index ad0c2b22282..e2480686490 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters @@ -313,6 +313,9 @@ ReactHost + + ReactHost + ReactHost diff --git a/vnext/Microsoft.ReactNative/ReactHost/DebuggerNotifications.h b/vnext/Microsoft.ReactNative/ReactHost/DebuggerNotifications.h new file mode 100644 index 00000000000..6a083140a34 --- /dev/null +++ b/vnext/Microsoft.ReactNative/ReactHost/DebuggerNotifications.h @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace Microsoft::ReactNative { + +struct DebuggerNotifications { + static winrt::Microsoft::ReactNative::IReactPropertyName ShowDebuggerPausedOverlayEventName() noexcept { + static winrt::Microsoft::ReactNative::IReactPropertyName propertyName{ + winrt::Microsoft::ReactNative::ReactPropertyBagHelper::GetName( + winrt::Microsoft::ReactNative::ReactPropertyBagHelper::GetNamespace(L"ReactNative.Debugger"), + L"ShowDebuggerPausedOverlay")}; + return propertyName; + } + + static void OnShowDebuggerPausedOverlay( + winrt::Microsoft::ReactNative::IReactNotificationService const &service, + std::string message, + std::function onResume) { + const winrt::Microsoft::ReactNative::ReactNonAbiValue>> nonAbiValue{ + std::in_place, std::tie(message, onResume)}; + service.SendNotification(ShowDebuggerPausedOverlayEventName(), nullptr, nonAbiValue); + } + + static void OnHideDebuggerPausedOverlay(winrt::Microsoft::ReactNative::IReactNotificationService const &service) { + service.SendNotification(ShowDebuggerPausedOverlayEventName(), nullptr, nullptr); + } + + static winrt::Microsoft::ReactNative::IReactNotificationSubscription SubscribeShowDebuggerPausedOverlay( + winrt::Microsoft::ReactNative::IReactNotificationService const &service, + winrt::Microsoft::ReactNative::IReactDispatcher const &dispatcher, + std::function)> showCallback, + std::function hideCallback) { + return service.Subscribe( + ShowDebuggerPausedOverlayEventName(), + dispatcher, + [showCallback, hideCallback](auto &&, winrt::Microsoft::ReactNative::IReactNotificationArgs const &args) { + if (args.Data()) { + const auto [message, onResume] = args.Data() + .as>>>() + .Value(); + showCallback(message, onResume); + } else { + hideCallback(); + } + }); + } +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/ReactHost/React.h b/vnext/Microsoft.ReactNative/ReactHost/React.h index 5a56fa70482..de9aae753cd 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/React.h +++ b/vnext/Microsoft.ReactNative/ReactHost/React.h @@ -32,6 +32,10 @@ #include #include +namespace facebook::react::jsinspector_modern { +class HostTarget; +} // namespace facebook::react::jsinspector_modern + namespace Mso::React { // Forward declarations @@ -343,6 +347,9 @@ struct ReactOptions { //! The callback is called when IReactInstance is destroyed and must not be used anymore. //! It is called from the native queue. OnReactInstanceDestroyedCallback OnInstanceDestroyed; + + //! The HostTarget instance for Fusebox + facebook::react::jsinspector_modern::HostTarget *InspectorTarget; }; //! IReactHost manages a ReactNative instance. diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp index 5251bedaafb..1283d156165 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp @@ -291,6 +291,18 @@ ReactInstanceWin::ReactInstanceWin( } ReactInstanceWin::~ReactInstanceWin() noexcept { +#ifdef USE_FABRIC + if (m_bridgelessReactInstance && m_options.InspectorTarget) { + auto messageDispatchQueue = + Mso::React::MessageDispatchQueue(::Microsoft::ReactNative::FuseboxInspectorThread::Instance(), nullptr); + messageDispatchQueue.runOnQueueSync([weakBridgelessReactInstance = std::weak_ptr(m_bridgelessReactInstance)]() { + if (auto bridgelessReactInstance = weakBridgelessReactInstance.lock()) { + bridgelessReactInstance->unregisterFromInspector(); + } + }); + } +#endif + std::scoped_lock lock{s_registryMutex}; auto it = std::find(s_instanceRegistry.begin(), s_instanceRegistry.end(), this); if (it != s_instanceRegistry.end()) { @@ -527,6 +539,8 @@ std::shared_ptr ReactInstanceWin::CreateDevSetting devSettings->useRuntimeScheduler = useRuntimeScheduler; + devSettings->inspectorTarget = m_options.InspectorTarget; + return devSettings; } @@ -618,8 +632,12 @@ void ReactInstanceWin::InitializeBridgeless() noexcept { devSettings, m_jsMessageThread.Load(), CreateHermesPreparedScriptStore()); auto jsRuntime = std::make_unique(m_jsiRuntimeHolder); jsRuntime->getRuntime(); - m_bridgelessReactInstance = std::make_unique( - std::move(jsRuntime), m_jsMessageThread.Load(), timerManager, jsErrorHandlingFunc); + m_bridgelessReactInstance = std::make_shared( + std::move(jsRuntime), + m_jsMessageThread.Load(), + timerManager, + jsErrorHandlingFunc, + m_options.InpectorTarget); auto bufferedRuntimeExecutor = m_bridgelessReactInstance->getBufferedRuntimeExecutor(); timerManager->setRuntimeExecutor(bufferedRuntimeExecutor); diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h index c043a5ce335..c64d2a41e58 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h @@ -204,7 +204,7 @@ class ReactInstanceWin final : public Mso::ActiveObject #ifdef USE_FABRIC // Bridgeless - std::unique_ptr m_bridgelessReactInstance; + std::shared_ptr m_bridgelessReactInstance; #endif std::atomic m_state{ReactInstanceState::Loading}; diff --git a/vnext/Microsoft.ReactNative/ReactNativeHost.cpp b/vnext/Microsoft.ReactNative/ReactNativeHost.cpp index 8fc98d62cc7..8d9c35d7555 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeHost.cpp +++ b/vnext/Microsoft.ReactNative/ReactNativeHost.cpp @@ -5,13 +5,16 @@ #include "ReactNativeHost.h" #include "ReactNativeHost.g.cpp" +#include "FuseboxInspectorThread.h" #include "ReactPackageBuilder.h" #include "RedBox.h" #include "TurboModulesProvider.h" #include +#include #include #include "IReactContext.h" +#include "ReactHost/DebuggerNotifications.h" #include "ReactInstanceSettings.h" #ifdef USE_FABRIC @@ -30,6 +33,39 @@ using namespace xaml::Controls; namespace winrt::Microsoft::ReactNative::implementation { +class FuseboxHostTargetDelegate : public facebook::react::jsinspector_modern::HostTargetDelegate, + public std::enable_shared_from_this { + public: + FuseboxHostTargetDelegate(ReactNativeHost *reactNativeHost) : m_reactNativeHost(reactNativeHost) {} + + void onReload(facebook::react::jsinspector_modern::HostTargetDelegate::PageReloadRequest const &request) override { + m_reactNativeHost->ReloadInstance(); + } + +#ifdef HAS_FUSEBOX_PAUSED_OVERLAY // Remove after syncing past https://github.com/facebook/react-native/pull/44078 + void onSetPausedInDebuggerMessage( + facebook::react::jsinspector_modern::HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest const &request) + override { + const auto instanceSettings = m_reactNativeHost->InstanceSettings(); + if (instanceSettings) { + if (request.message.has_value()) { + ::Microsoft::ReactNative::DebuggerNotifications::OnShowDebuggerPausedOverlay( + instanceSettings.Notifications(), request.message.value(), [weakThis = weak_from_this()]() { + if (auto strongThis = weakThis.lock()) { + strongThis->m_reactNativeHost->OnDebuggerResume(); + } + }); + } else { + ::Microsoft::ReactNative::DebuggerNotifications::OnHideDebuggerPausedOverlay(instanceSettings.Notifications()); + } + } + } +#endif + + private: + ReactNativeHost *m_reactNativeHost; +}; + ReactNativeHost::ReactNativeHost() noexcept : m_reactHost{Mso::React::MakeReactHost()} { #if _DEBUG facebook::react::InitializeLogging([](facebook::react::RCTLogLevel /*logLevel*/, const char *message) { @@ -37,6 +73,45 @@ ReactNativeHost::ReactNativeHost() noexcept : m_reactHost{Mso::React::MakeReactH OutputDebugStringA(str.c_str()); }); #endif + + auto &inspectorFlags = facebook::react::jsinspector_modern::InspectorFlags::getInstance(); + if (inspectorFlags.getEnableModernCDPRegistry() && !m_inspectorPageId.has_value()) { + m_inspectorHostDelegate = std::make_shared(this); + m_inspectorTarget = facebook::react::jsinspector_modern::HostTarget::create( + *m_inspectorHostDelegate, [](std::function &&callback) { + ::Microsoft::ReactNative::FuseboxInspectorThread::Instance().InvokeElsePost([callback]() { callback(); }); + }); + + std::weak_ptr weakInspectorTarget = m_inspectorTarget; + facebook::react::jsinspector_modern::InspectorTargetCapabilities capabilities; +#ifdef HAS_FUSEBOX_CAPABILITIES // Remove after syncing past https://github.com/facebook/react-native/pull/43689 + capabilities.nativePageReloads = true; + capabilities.prefersFuseboxFrontend = true; +#endif + m_inspectorPageId = facebook::react::jsinspector_modern::getInspectorInstance().addPage( + "React Native Windows (Experimental)", + /* vm */ "", + [weakInspectorTarget](std::unique_ptr remote) + -> std::unique_ptr { + if (const auto inspectorTarget = weakInspectorTarget.lock()) { + facebook::react::jsinspector_modern::HostTarget::SessionMetadata sessionMetadata; + sessionMetadata.integrationName = "React Native Windows (Host)"; + return inspectorTarget->connect(std::move(remote), sessionMetadata); + } + + // This can happen if we're about to be dealloc'd. Reject the connection. + return nullptr; + }, + capabilities); + } +} + +ReactNativeHost::~ReactNativeHost() noexcept { + if (m_inspectorPageId.has_value()) { + facebook::react::jsinspector_modern::getInspectorInstance().removePage(*m_inspectorPageId); + m_inspectorPageId.reset(); + m_inspectorTarget.reset(); + } } /*static*/ ReactNative::ReactNativeHost ReactNativeHost::FromContext( @@ -186,6 +261,7 @@ IAsyncAction ReactNativeHost::ReloadInstance() noexcept { } reactOptions.Identity = jsBundleFile; + reactOptions.InspectorTarget = m_inspectorTarget.get(); return make(m_reactHost->ReloadInstanceWithOptions(std::move(reactOptions))); } @@ -197,4 +273,15 @@ Mso::React::IReactHost *ReactNativeHost::ReactHost() noexcept { return m_reactHost.Get(); } +void ReactNativeHost::OnDebuggerResume() noexcept { +#ifdef HAS_FUSEBOX_PAUSED_OVERLAY // Remove after syncing past https://github.com/facebook/react-native/pull/44078 + ::Microsoft::ReactNative::FuseboxInspectorThread::Instance().InvokeElsePost( + [weakInspectorTarget = std::weak_ptr(m_inspectorTarget)]() { + if (const auto inspectorTarget = weakInspectorTarget.lock()) { + inspectorTarget->sendCommand(facebook::react::jsinspector_modern::HostCommand::DebuggerResume); + } + }); +#endif +} + } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/ReactNativeHost.h b/vnext/Microsoft.ReactNative/ReactNativeHost.h index d133e8bd883..a78894039a3 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeHost.h +++ b/vnext/Microsoft.ReactNative/ReactNativeHost.h @@ -5,6 +5,7 @@ #include "ReactNativeHost.g.h" +#include #include "NativeModulesProvider.h" #include "ReactHost/React.h" #include "ReactInstanceSettings.h" @@ -16,6 +17,7 @@ namespace winrt::Microsoft::ReactNative::implementation { struct ReactNativeHost : ReactNativeHostT { public: // ReactNativeHost ABI API ReactNativeHost() noexcept; + ~ReactNativeHost() noexcept; static ReactNative::ReactNativeHost FromContext(ReactNative::IReactContext const &reactContext) noexcept; @@ -25,6 +27,7 @@ struct ReactNativeHost : ReactNativeHostT { // property InstanceSettings ReactNative::ReactInstanceSettings InstanceSettings() noexcept; void InstanceSettings(ReactNative::ReactInstanceSettings const &value) noexcept; + void OnDebuggerResume() noexcept; winrt::Windows::Foundation::IAsyncAction LoadInstance() noexcept; winrt::Windows::Foundation::IAsyncAction ReloadInstance() noexcept; @@ -39,6 +42,10 @@ struct ReactNativeHost : ReactNativeHostT { ReactNative::ReactInstanceSettings m_instanceSettings{nullptr}; ReactNative::IReactPackageBuilder m_packageBuilder; + + std::shared_ptr m_inspectorHostDelegate{nullptr}; + std::shared_ptr m_inspectorTarget{nullptr}; + std::optional m_inspectorPageId{std::nullopt}; }; } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/ReactCommon/ReactCommon.vcxproj b/vnext/ReactCommon/ReactCommon.vcxproj index b299a156afa..dafce915a9a 100644 --- a/vnext/ReactCommon/ReactCommon.vcxproj +++ b/vnext/ReactCommon/ReactCommon.vcxproj @@ -109,6 +109,7 @@ + @@ -131,6 +132,7 @@ + diff --git a/vnext/Shared/DevSettings.h b/vnext/Shared/DevSettings.h index 0d21853a7f2..7cd0af57f25 100644 --- a/vnext/Shared/DevSettings.h +++ b/vnext/Shared/DevSettings.h @@ -23,6 +23,10 @@ struct RuntimeHolderLazyInit; namespace facebook { namespace react { +namespace jsinspector_modern { +class HostTarget; +} // namespace jsinspector_modern + enum class JSIEngineOverride : int32_t { Default = 0, // No JSI, will use the legacy ExecutorFactory Chakra = 1, // Use the JSIExecutorFactory with ChakraRuntime @@ -114,6 +118,9 @@ struct DevSettings { // Enable concurrent mode by installing runtimeScheduler bool useRuntimeScheduler{false}; + + // The HostTarget instance for Fusebox + facebook::react::jsinspector_modern::HostTarget *inspectorTarget; }; } // namespace react diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index 1ce98cf6534..f32385af3af 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -486,7 +486,8 @@ InstanceImpl::InstanceImpl( } } - m_innerInstance->initializeBridge(std::move(callback), jsef, m_jsThread, m_moduleRegistry); + m_innerInstance->initializeBridge( + std::move(callback), jsef, m_jsThread, m_moduleRegistry, m_devSettings->inspectorTarget); // For RuntimeScheduler to work properly, we need to install TurboModuleManager with RuntimeSchedulerCallbackInvoker. // To be able to do that, we need to be able to call m_innerInstance->getRuntimeExecutor(), which we can only do after @@ -589,6 +590,16 @@ void InstanceImpl::loadBundleInternal(std::string &&jsBundleRelativePath, bool s } InstanceImpl::~InstanceImpl() { + if (m_devSettings->inspectorTarget) { + auto messageDispatchQueue = + Mso::React::MessageDispatchQueue(::Microsoft::ReactNative::FuseboxInspectorThread::Instance(), nullptr); + messageDispatchQueue.runOnQueueSync([weakInnerInstance = std::weak_ptr(m_innerInstance)]() { + if (auto innerInstance = weakInnerInstance.lock()) { + innerInstance->unregisterFromInspector(); + } + }); + } + if (shouldStartHermesInspector(*m_devSettings) && m_devSettings->jsiRuntimeHolder) { m_devSettings->jsiRuntimeHolder->teardown(); } From 0b83a21064f844c50de0bc04b0dd53ae894f0c1b Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Mon, 29 Apr 2024 13:29:22 -0400 Subject: [PATCH 4/9] Add InspectorPackagerConnectionDelegate implementation This adds an implementation of InspectorPackagerConnectionDelegate for react-native-windows. --- ...boxInspectorPackagerConnectionDelegate.cpp | 76 +++++++++++++++++++ ...seboxInspectorPackagerConnectionDelegate.h | 32 ++++++++ vnext/Shared/Shared.vcxitems | 2 + vnext/Shared/Shared.vcxitems.filters | 6 ++ 4 files changed, 116 insertions(+) create mode 100644 vnext/Shared/FuseboxInspectorPackagerConnectionDelegate.cpp create mode 100644 vnext/Shared/FuseboxInspectorPackagerConnectionDelegate.h diff --git a/vnext/Shared/FuseboxInspectorPackagerConnectionDelegate.cpp b/vnext/Shared/FuseboxInspectorPackagerConnectionDelegate.cpp new file mode 100644 index 00000000000..9113a4f24e9 --- /dev/null +++ b/vnext/Shared/FuseboxInspectorPackagerConnectionDelegate.cpp @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "FuseboxInspectorPackagerConnectionDelegate.h" + +#include +#include "FuseboxInspectorThread.h" + +namespace Microsoft::ReactNative { + +FuseboxInspectorPackagerConnectionDelegate::WebSocket::WebSocket( + std::string const &url, + std::weak_ptr delegate) + : m_weakDelegate{delegate} { + std::vector certExceptions; + + m_packagerWebSocketConnection = + std::make_shared(std::move(certExceptions)); + + m_packagerWebSocketConnection->SetOnMessage([delegate](auto &&, const std::string &message, bool isBinary) { + FuseboxInspectorThread::Instance().InvokeElsePost([delegate, message]() { + if (const auto strongDelegate = delegate.lock()) { + strongDelegate->didReceiveMessage(message); + } + }); + }); + m_packagerWebSocketConnection->SetOnError( + [delegate](const Microsoft::React::Networking::IWebSocketResource::Error &error) { + FuseboxInspectorThread::Instance().InvokeElsePost([delegate, error]() { + if (const auto strongDelegate = delegate.lock()) { + strongDelegate->didFailWithError(std::nullopt, error.Message); + } + }); + }); + m_packagerWebSocketConnection->SetOnClose([delegate](auto &&...) { + FuseboxInspectorThread::Instance().InvokeElsePost([delegate]() { + if (const auto strongDelegate = delegate.lock()) { + strongDelegate->didClose(); + } + }); + }); + + Microsoft::React::Networking::IWebSocketResource::Protocols protocols; + Microsoft::React::Networking::IWebSocketResource::Options options; + m_packagerWebSocketConnection->Connect(std::string{url}, protocols, options); +} + +void FuseboxInspectorPackagerConnectionDelegate::WebSocket::send(std::string_view message) { + m_packagerWebSocketConnection->Send(std::string{message}); +} + +FuseboxInspectorPackagerConnectionDelegate::WebSocket::~WebSocket() { + std::string reason{"Explicit close"}; + m_packagerWebSocketConnection->Close( + Microsoft::React::Networking::WinRTWebSocketResource::CloseCode::GoingAway, reason); +} + +std::unique_ptr +FuseboxInspectorPackagerConnectionDelegate::connectWebSocket( + const std::string &url, + std::weak_ptr delegate) { + return std::make_unique(url, delegate); +} + +winrt::fire_and_forget RunWithDelayAsync(std::function callback, std::chrono::milliseconds delayMs) { + co_await winrt::resume_after(delayMs); + FuseboxInspectorThread::Instance().InvokeElsePost([callback]() { callback(); }); +} + +void FuseboxInspectorPackagerConnectionDelegate::scheduleCallback( + std::function callback, + std::chrono::milliseconds delayMs) { + RunWithDelayAsync(callback, delayMs); +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/FuseboxInspectorPackagerConnectionDelegate.h b/vnext/Shared/FuseboxInspectorPackagerConnectionDelegate.h new file mode 100644 index 00000000000..27ff809f9be --- /dev/null +++ b/vnext/Shared/FuseboxInspectorPackagerConnectionDelegate.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include + +namespace Microsoft::ReactNative { + +class FuseboxInspectorPackagerConnectionDelegate final + : public facebook::react::jsinspector_modern::InspectorPackagerConnectionDelegate { + class WebSocket : public facebook::react::jsinspector_modern::IWebSocket { + public: + WebSocket(std::string const &url, std::weak_ptr delegate); + virtual void send(std::string_view message) override; + virtual ~WebSocket() override; + + private: + std::shared_ptr m_packagerWebSocketConnection; + std::weak_ptr m_weakDelegate; + }; + + public: + virtual std::unique_ptr connectWebSocket( + const std::string &url, + std::weak_ptr delegate) override; + + virtual void scheduleCallback(std::function callback, std::chrono::milliseconds delayMs) override; +}; +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index ebc62c54cc8..120b0873510 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -218,6 +218,7 @@ + @@ -415,6 +416,7 @@ + diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index 1dd765e3906..64d6757edd7 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -94,6 +94,9 @@ Source Files\JSI + + Source Files + Source Files @@ -613,6 +616,9 @@ Header Files\JSI + + Header Files + Header Files From 7d197df318a16a58b49844bf7a9d285521a5c451 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Mon, 29 Apr 2024 13:39:09 -0400 Subject: [PATCH 5/9] Wire up Fusebox InspectorPackagerConnection Using the same inspector thread initialized in ReactNativeHost, this change wires up the InspectorPackagerConnectionDelegate to use when initializatng the InspectorPackagerConnection from ReactCommon and conditionally sets it for Hermes direct debugging when the appropriate feature flag is set. --- vnext/ReactCommon/ReactCommon.vcxproj | 2 ++ vnext/Shared/DevSupportManager.cpp | 29 ++++++++++++++++++--------- vnext/Shared/DevSupportManager.h | 3 +++ vnext/Shared/IDevSupportManager.h | 1 + 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/vnext/ReactCommon/ReactCommon.vcxproj b/vnext/ReactCommon/ReactCommon.vcxproj index dafce915a9a..0e17e4ae0b7 100644 --- a/vnext/ReactCommon/ReactCommon.vcxproj +++ b/vnext/ReactCommon/ReactCommon.vcxproj @@ -111,6 +111,7 @@ + @@ -134,6 +135,7 @@ + diff --git a/vnext/Shared/DevSupportManager.cpp b/vnext/Shared/DevSupportManager.cpp index 29eb9f22c61..8029b22f31b 100644 --- a/vnext/Shared/DevSupportManager.cpp +++ b/vnext/Shared/DevSupportManager.cpp @@ -9,12 +9,14 @@ #include #include +#include "FuseboxInspectorPackagerConnectionDelegate.h" #include "PackagerConnection.h" #include "Unicode.h" #include "Utilities.h" #include +#include #include #include #include @@ -273,12 +275,10 @@ void DevSupportManager::EnsureHermesInspector( [[maybe_unused]] const uint16_t packagerPort, [[maybe_unused]] const std::string &bundleAppId) noexcept { static std::once_flag once; - std::call_once(once, [this, &packagerHost, packagerPort, &jsBundleName]() { - // TODO: should we use the bundleAppId as the app param if available? - + std::call_once(once, [this, &packagerHost, packagerPort, &bundleAppId]() { std::string packageName{bundleAppId}; - if (packageName == "") { - std::string packageName("RNW"); + if (packageName.empty()) { + packageName = "RNW"; wchar_t fullName[PACKAGE_FULL_NAME_MAX_LENGTH]{}; UINT32 size = ARRAYSIZE(fullName); if (SUCCEEDED(GetCurrentPackageFullName(&size, fullName))) { @@ -294,11 +294,20 @@ void DevSupportManager::EnsureHermesInspector( } const auto deviceId = GetDeviceId(packageName); - m_inspectorPackagerConnection = std::make_shared( - facebook::react::DevServerHelper::get_InspectorDeviceUrl( - packagerHost, packagerPort, deviceName, packageName, deviceId), - m_BundleStatusProvider); - m_inspectorPackagerConnection->connectAsync(); + auto inspectorUrl = facebook::react::DevServerHelper::get_InspectorDeviceUrl( + packagerHost, packagerPort, deviceName, packageName, deviceId); + auto &inspectorFlags = jsinspector_modern::InspectorFlags::getInstance(); + if (inspectorFlags.getEnableCxxInspectorPackagerConnection()) { + m_fuseboxInspectorPackagerConnection = std::make_unique( + inspectorUrl, + packageName, + std::make_unique()); + m_fuseboxInspectorPackagerConnection->connect(); + } else { + m_inspectorPackagerConnection = + std::make_shared(std::move(inspectorUrl), m_BundleStatusProvider); + m_inspectorPackagerConnection->connectAsync(); + } }); } diff --git a/vnext/Shared/DevSupportManager.h b/vnext/Shared/DevSupportManager.h index 6e25131baa3..fcf1880cfd6 100644 --- a/vnext/Shared/DevSupportManager.h +++ b/vnext/Shared/DevSupportManager.h @@ -15,6 +15,7 @@ #include #include +#include namespace facebook { namespace react { @@ -59,6 +60,8 @@ class DevSupportManager final : public facebook::react::IDevSupportManager { std::atomic_bool m_cancellation_token; std::shared_ptr m_inspectorPackagerConnection; + std::unique_ptr + m_fuseboxInspectorPackagerConnection; struct BundleStatusProvider : public InspectorPackagerConnection::IBundleStatusProvider { virtual InspectorPackagerConnection::BundleStatus getBundleStatus() { diff --git a/vnext/Shared/IDevSupportManager.h b/vnext/Shared/IDevSupportManager.h index 4ebde10186e..9f2fd6f53ae 100644 --- a/vnext/Shared/IDevSupportManager.h +++ b/vnext/Shared/IDevSupportManager.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include From 4740af35da06885daa292b3c5d71af3ea09c1081 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Mon, 29 Apr 2024 13:46:42 -0400 Subject: [PATCH 6/9] Change files --- ...ative-windows-5fdef670-daf7-48a0-978f-27aba2e64394.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-5fdef670-daf7-48a0-978f-27aba2e64394.json diff --git a/change/react-native-windows-5fdef670-daf7-48a0-978f-27aba2e64394.json b/change/react-native-windows-5fdef670-daf7-48a0-978f-27aba2e64394.json new file mode 100644 index 00000000000..c0b0bc74b3b --- /dev/null +++ b/change/react-native-windows-5fdef670-daf7-48a0-978f-27aba2e64394.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Wire up Fusebox InspectorPackagerConnection", + "packageName": "react-native-windows", + "email": "erozell@outlook.com", + "dependentChangeType": "patch" +} From bfed766f918724d703f550d44bfd337cd7c263e0 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Mon, 29 Apr 2024 14:07:54 -0400 Subject: [PATCH 7/9] Example wiring up the inspector flag --- .../playground-win32/Playground-Win32.cpp | 1 + vnext/Microsoft.ReactNative/QuirkSettings.cpp | 20 +++++++++++++++++++ vnext/Microsoft.ReactNative/QuirkSettings.h | 2 ++ vnext/Microsoft.ReactNative/QuirkSettings.idl | 5 +++++ 4 files changed, 28 insertions(+) diff --git a/packages/playground/windows/playground-win32/Playground-Win32.cpp b/packages/playground/windows/playground-win32/Playground-Win32.cpp index a3c45b43f68..5e068ff2447 100644 --- a/packages/playground/windows/playground-win32/Playground-Win32.cpp +++ b/packages/playground/windows/playground-win32/Playground-Win32.cpp @@ -85,6 +85,7 @@ struct WindowData { winrt::Microsoft::ReactNative::ReactNativeHost Host() noexcept { if (!m_host) { + winrt::Microsoft::ReactNative::QuirkSettings::SetUseFusebox(true); m_host = winrt::Microsoft::ReactNative::ReactNativeHost(); m_host.InstanceSettings(InstanceSettings()); } diff --git a/vnext/Microsoft.ReactNative/QuirkSettings.cpp b/vnext/Microsoft.ReactNative/QuirkSettings.cpp index 81dddc80a82..82ba20df624 100644 --- a/vnext/Microsoft.ReactNative/QuirkSettings.cpp +++ b/vnext/Microsoft.ReactNative/QuirkSettings.cpp @@ -9,10 +9,26 @@ #include "React.h" #include "ReactPropertyBag.h" +#include +#include + namespace winrt::Microsoft::ReactNative::implementation { QuirkSettings::QuirkSettings() noexcept {} +class QuirkSettingsReactNativeFeatureFlags : public facebook::react::ReactNativeFeatureFlagsDefaults { + public: + QuirkSettingsReactNativeFeatureFlags(bool enableModernCDPRegistry) + : m_enableModernCDPRegistry(enableModernCDPRegistry) {} + + bool inspectorEnableModernCDPRegistry() override { + return m_enableModernCDPRegistry; + } + + private: + bool m_enableModernCDPRegistry; +}; + winrt::Microsoft::ReactNative::ReactPropertyId MatchAndroidAndIOSStretchBehaviorProperty() noexcept { static winrt::Microsoft::ReactNative::ReactPropertyId propId{ L"ReactNative.QuirkSettings", L"MatchAndroidAndIOSyStretchBehavior"}; @@ -137,6 +153,10 @@ winrt::Microsoft::ReactNative::ReactPropertyId IsBridgelessProperty() noex ReactPropertyBag(settings.Properties()).Set(UseRuntimeSchedulerProperty(), value); } +/*static*/ void QuirkSettings::SetUseFusebox(bool value) noexcept { + facebook::react::ReactNativeFeatureFlags::override(std::make_unique(value)); +} + #pragma endregion IDL interface /*static*/ bool QuirkSettings::GetMatchAndroidAndIOSStretchBehavior(ReactPropertyBag properties) noexcept { diff --git a/vnext/Microsoft.ReactNative/QuirkSettings.h b/vnext/Microsoft.ReactNative/QuirkSettings.h index 0c5d73e94e4..75469a420d1 100644 --- a/vnext/Microsoft.ReactNative/QuirkSettings.h +++ b/vnext/Microsoft.ReactNative/QuirkSettings.h @@ -68,6 +68,8 @@ struct QuirkSettings : QuirkSettingsT { winrt::Microsoft::ReactNative::ReactInstanceSettings settings, bool value) noexcept; + static void SetUseFusebox(bool value) noexcept; + #pragma endregion Public API - part of IDL interface }; diff --git a/vnext/Microsoft.ReactNative/QuirkSettings.idl b/vnext/Microsoft.ReactNative/QuirkSettings.idl index a4ad798aced..ed398025afb 100644 --- a/vnext/Microsoft.ReactNative/QuirkSettings.idl +++ b/vnext/Microsoft.ReactNative/QuirkSettings.idl @@ -64,6 +64,11 @@ namespace Microsoft.ReactNative "By default `react-native-windows` will use the new RuntimeScheduler." "Setting this to false will revert the behavior to previous scheduling logic.") static void SetUseRuntimeScheduler(ReactInstanceSettings settings, Boolean value); + + DOC_STRING( + "By default `react-native-windows` uses the legacy inspector packager connection protocol." + "Setting this to true to enable the modern \"Fusebox\" debugging functionality.") + static void SetUseFusebox(Boolean value); } } // namespace Microsoft.ReactNative From 70881c13e7b79977bcb49c1d4b78589f7fa4b349 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Wed, 1 May 2024 19:04:02 -0400 Subject: [PATCH 8/9] Wire up debugger paused overlay To avoid conflicting with content manipulation of the ReactRootView, this change uses a Flyout anchored to the top of the ReactRootView to show the debugger paused overlay. It disables the default light dismiss (and automatic dismissal when the window loses focus) by overriding the Closing method and canceling the close event until we receive a hide debugger overlay notification. --- vnext/Microsoft.ReactNative/ReactRootView.cpp | 76 +++++++++++++++++++ vnext/Microsoft.ReactNative/ReactRootView.h | 4 + 2 files changed, 80 insertions(+) diff --git a/vnext/Microsoft.ReactNative/ReactRootView.cpp b/vnext/Microsoft.ReactNative/ReactRootView.cpp index a697f76aa39..edee6c70eee 100644 --- a/vnext/Microsoft.ReactNative/ReactRootView.cpp +++ b/vnext/Microsoft.ReactNative/ReactRootView.cpp @@ -5,7 +5,10 @@ #include "ReactRootView.g.cpp" #include +#include #include +#include +#include #include #include #include @@ -45,6 +48,20 @@ void ReactRootView::ReactNativeHost(ReactNative::ReactNativeHost const &value) n if (m_reactNativeHost != value) { ReactViewHost(nullptr); m_reactNativeHost = value; + const auto weakThis = this->get_weak(); + ::Microsoft::ReactNative::DebuggerNotifications::SubscribeShowDebuggerPausedOverlay( + m_reactNativeHost.InstanceSettings().Notifications(), + m_reactNativeHost.InstanceSettings().UIDispatcher(), + [weakThis](std::string message, std::function onResume) { + if (auto strongThis = weakThis.get()) { + strongThis->ShowDebuggerPausedOverlay(message, onResume); + } + }, + [weakThis]() { + if (auto strongThis = weakThis.get()) { + strongThis->HideDebuggerPausedOverlay(); + } + }); ReloadView(); } } @@ -283,6 +300,65 @@ void ReactRootView::EnsureLoadingUI() noexcept { } } +void ReactRootView::HideDebuggerPausedOverlay() noexcept { + m_isDebuggerPausedOverlayOpen = false; + if (m_debuggerPausedFlyout) { + m_debuggerPausedFlyout.Hide(); + m_debuggerPausedFlyout = nullptr; + } +} + +void ReactRootView::ShowDebuggerPausedOverlay( + const std::string &message, + const std::function &onResume) noexcept { + // Initialize content + const xaml::Controls::Grid contentGrid; + xaml::Controls::ColumnDefinition messageColumnDefinition; + xaml::Controls::ColumnDefinition buttonColumnDefinition; + messageColumnDefinition.MinWidth(60); + buttonColumnDefinition.MinWidth(36); + contentGrid.ColumnDefinitions().Append(messageColumnDefinition); + contentGrid.ColumnDefinitions().Append(buttonColumnDefinition); + xaml::Controls::TextBlock messageBlock; + messageBlock.Text(winrt::to_hstring(message)); + messageBlock.FontWeight(winrt::Windows::UI::Text::FontWeights::SemiBold()); + xaml::Controls::FontIcon resumeGlyph; + resumeGlyph.FontFamily(xaml::Media::FontFamily(L"Segoe MDL2 Assets")); + resumeGlyph.Foreground(xaml::Media::SolidColorBrush(winrt::Colors::Green())); + resumeGlyph.Glyph(L"\uF5B0"); + resumeGlyph.HorizontalAlignment(xaml::HorizontalAlignment::Right); + resumeGlyph.PointerReleased([onResume](auto &&...) { onResume(); }); + xaml::Controls::Grid::SetColumn(resumeGlyph, 1); + contentGrid.Children().Append(messageBlock); + contentGrid.Children().Append(resumeGlyph); + + // Configure flyout + m_isDebuggerPausedOverlayOpen = true; + xaml::Style flyoutStyle( + {XAML_NAMESPACE_STR L".Controls.FlyoutPresenter", winrt::Windows::UI::Xaml::Interop::TypeKind::Metadata}); + flyoutStyle.Setters().Append(winrt::Setter( + xaml::Controls::Control::CornerRadiusProperty(), winrt::box_value(xaml::CornerRadius{12, 12, 12, 12}))); + flyoutStyle.Setters().Append(winrt::Setter( + xaml::Controls::Control::BackgroundProperty(), + winrt::box_value(xaml::Media::SolidColorBrush{FromArgb(255, 255, 255, 193)}))); + flyoutStyle.Setters().Append( + winrt::Setter(xaml::FrameworkElement::MarginProperty(), winrt::box_value(xaml::Thickness{0, 12, 0, 0}))); + m_debuggerPausedFlyout = xaml::Controls::Flyout{}; + m_debuggerPausedFlyout.FlyoutPresenterStyle(flyoutStyle); + m_debuggerPausedFlyout.LightDismissOverlayMode(xaml::Controls::LightDismissOverlayMode::On); + m_debuggerPausedFlyout.Content(contentGrid); + + // Disable light dismiss + m_debuggerPausedFlyout.Closing([weakThis = this->get_weak()](auto &&, const auto &args) { + if (auto strongThis = weakThis.get()) { + args.Cancel(strongThis->m_isDebuggerPausedOverlayOpen); + } + }); + + // Show flyout + m_debuggerPausedFlyout.ShowAt(*this); +} + void ReactRootView::ShowInstanceLoaded() noexcept { if (m_xamlRootView) { ClearLoadingUI(); diff --git a/vnext/Microsoft.ReactNative/ReactRootView.h b/vnext/Microsoft.ReactNative/ReactRootView.h index f35a3c3fd60..9481346ffb9 100644 --- a/vnext/Microsoft.ReactNative/ReactRootView.h +++ b/vnext/Microsoft.ReactNative/ReactRootView.h @@ -72,6 +72,7 @@ struct ReactRootView : ReactRootViewT, ::Microsoft::ReactNative:: bool m_isPerspectiveEnabled{true}; bool m_isInitialized{false}; bool m_isJSViewAttached{false}; + bool m_isDebuggerPausedOverlayOpen{false}; Mso::DispatchQueue m_uiQueue; int64_t m_rootTag{-1}; std::unique_ptr m_reactOptions; @@ -84,6 +85,7 @@ struct ReactRootView : ReactRootViewT, ::Microsoft::ReactNative:: std::shared_ptr<::Microsoft::ReactNative::PreviewKeyboardEventHandlerOnRoot> m_previewKeyboardEventHandlerOnRoot; xaml::Controls::ContentControl m_focusSafeHarbor{nullptr}; xaml::Controls::ContentControl::LosingFocus_revoker m_focusSafeHarborLosingFocusRevoker{}; + xaml::Controls::Flyout m_debuggerPausedFlyout{nullptr}; winrt::Grid m_greenBoxGrid{nullptr}; winrt::TextBlock m_waitingTextBlock{nullptr}; winrt::SystemNavigationManager::BackRequested_revoker m_backRequestedRevoker{}; @@ -102,6 +104,8 @@ struct ReactRootView : ReactRootViewT, ::Microsoft::ReactNative:: void UpdateRootViewInternal() noexcept; void ClearLoadingUI() noexcept; void EnsureLoadingUI() noexcept; + void HideDebuggerPausedOverlay() noexcept; + void ShowDebuggerPausedOverlay(const std::string &message, const std::function &onResume) noexcept; void ShowInstanceLoaded() noexcept; void ShowInstanceError() noexcept; void ShowInstanceWaiting() noexcept; From 102cf15fc02d46c87e08572dfb7372e17d3d8a56 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Wed, 1 May 2024 19:16:07 -0400 Subject: [PATCH 9/9] Add Ctrl+Shift+I shortcut to open dev tools Similar to browsers, this change adds the Ctrl+Shift+I shortcut to each XamlRoot containing a ReactRootView. For simplicity, this only works with metro on localhost:8081, but we (or Microsoft) can revisit this in the future if we want to add more granular control over the host and port for metro. --- vnext/Microsoft.ReactNative/ReactRootView.cpp | 32 ++++++++++++ vnext/Microsoft.ReactNative/ReactRootView.h | 2 + vnext/Shared/DevServerHelper.h | 7 +++ vnext/Shared/DevSupportManager.cpp | 51 +++++++++++++------ vnext/Shared/DevSupportManager.h | 1 + vnext/Shared/IDevSupportManager.h | 1 + 6 files changed, 79 insertions(+), 15 deletions(-) diff --git a/vnext/Microsoft.ReactNative/ReactRootView.cpp b/vnext/Microsoft.ReactNative/ReactRootView.cpp index edee6c70eee..e04b0b813c2 100644 --- a/vnext/Microsoft.ReactNative/ReactRootView.cpp +++ b/vnext/Microsoft.ReactNative/ReactRootView.cpp @@ -14,8 +14,10 @@ #include #include #include +#include "InstanceManager.h" #include "ReactNativeHost.h" #include "ReactViewInstance.h" +#include "Utils/KeyboardUtils.h" #include "XamlUtils.h" #include @@ -37,6 +39,7 @@ ReactRootView::ReactRootView() noexcept : m_uiQueue(Mso::DispatchQueue::GetCurre UpdatePerspective(); Loaded([this](auto &&, auto &&) { ::Microsoft::ReactNative::SetCompositor(::Microsoft::ReactNative::GetCompositor(*this)); + SetupDevToolsShortcut(); }); } @@ -557,4 +560,33 @@ void ReactRootView::RemoveChildAt(uint32_t index) { Children().RemoveAt(RNIndexToXamlIndex(index)); } +bool IsCtrlShiftI(winrt::Windows::System::VirtualKey key) noexcept { + return ( + key == winrt::Windows::System::VirtualKey::I && + ::Microsoft::ReactNative::IsModifiedKeyPressed( + winrt::CoreWindow::GetForCurrentThread(), winrt::Windows::System::VirtualKey::Shift) && + ::Microsoft::ReactNative::IsModifiedKeyPressed( + winrt::CoreWindow::GetForCurrentThread(), winrt::Windows::System::VirtualKey::Control)); +} + +void ReactRootView::SetupDevToolsShortcut() noexcept { + if (auto xamlRoot = XamlRoot()) { + if (std::find(m_subscribedDebuggerRoots.begin(), m_subscribedDebuggerRoots.end(), xamlRoot) == + m_subscribedDebuggerRoots.end()) { + if (auto rootContent = xamlRoot.Content()) { + m_subscribedDebuggerRoots.push_back(xamlRoot); + rootContent.KeyDown( + [weakThis = this->get_weak()](const auto & /*sender*/, const xaml::Input::KeyRoutedEventArgs &args) { + if (const auto strongThis = weakThis.get()) { + if (IsCtrlShiftI(args.Key())) { + ::Microsoft::ReactNative::GetSharedDevManager()->OpenDevTools( + winrt::to_string(strongThis->m_reactNativeHost.InstanceSettings().BundleAppId())); + } + }; + }); + } + } + } +} + } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/ReactRootView.h b/vnext/Microsoft.ReactNative/ReactRootView.h index 9481346ffb9..876ac7e38cb 100644 --- a/vnext/Microsoft.ReactNative/ReactRootView.h +++ b/vnext/Microsoft.ReactNative/ReactRootView.h @@ -89,6 +89,7 @@ struct ReactRootView : ReactRootViewT, ::Microsoft::ReactNative:: winrt::Grid m_greenBoxGrid{nullptr}; winrt::TextBlock m_waitingTextBlock{nullptr}; winrt::SystemNavigationManager::BackRequested_revoker m_backRequestedRevoker{}; + std::vector m_subscribedDebuggerRoots{}; // Visual tree to support safe harbor // this @@ -116,6 +117,7 @@ struct ReactRootView : ReactRootViewT, ::Microsoft::ReactNative:: bool OnBackRequested() noexcept; Mso::React::IReactViewHost *ReactViewHost() noexcept; void ReactViewHost(Mso::React::IReactViewHost *viewHost) noexcept; + void SetupDevToolsShortcut() noexcept; }; } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Shared/DevServerHelper.h b/vnext/Shared/DevServerHelper.h index 67587e2b521..55ce1881638 100644 --- a/vnext/Shared/DevServerHelper.h +++ b/vnext/Shared/DevServerHelper.h @@ -86,6 +86,12 @@ class DevServerHelper { deviceId.c_str()); } + static std::string + get_OpenDebuggerUrl(const std::string &packagerHost, const uint16_t packagerPort, const std::string &deviceId) { + return string_format( + OpenDebuggerUrlFormat, GetDeviceLocalHost(packagerHost, packagerPort).c_str(), deviceId.c_str()); + } + static constexpr const char DefaultPackagerHost[] = "localhost"; static const uint16_t DefaultPackagerPort = 8081; @@ -108,6 +114,7 @@ class DevServerHelper { static constexpr const char PackagerStatusUrlFormat[] = "http://%s/status"; static constexpr const char PackagerOpenStackFrameUrlFormat[] = "https://%s/open-stack-frame"; static constexpr const char InspectorDeviceUrlFormat[] = "ws://%s/inspector/device?name=%s&app=%s&device=%s"; + static constexpr const char OpenDebuggerUrlFormat[] = "http://%s/open-debugger?device=%s"; static constexpr const char PackagerOkStatus[] = "packager-status:running"; const int LongPollFailureDelayMs = 5000; diff --git a/vnext/Shared/DevSupportManager.cpp b/vnext/Shared/DevSupportManager.cpp index 8029b22f31b..9620230cd0a 100644 --- a/vnext/Shared/DevSupportManager.cpp +++ b/vnext/Shared/DevSupportManager.cpp @@ -176,7 +176,7 @@ bool IsIgnorablePollHResult(HRESULT hr) { return hr == WININET_E_INVALID_SERVER_RESPONSE; } -std::string GetDeviceId(const std::string &bundleAppId) { +std::string GetDeviceId(const std::string &packageName) { const auto hash = winrt::Windows::Security::Cryptography::Core::HashAlgorithmProvider::OpenAlgorithm( winrt::Windows::Security::Cryptography::Core::HashAlgorithmNames::Sha256()) .CreateHash(); @@ -186,9 +186,9 @@ std::string GetDeviceId(const std::string &bundleAppId) { // If an app ID is provided, we will allow reconnection to DevTools. // Apps must supply a unique app ID to each ReactNativeHost instance settings for this to behave correctly. if (!bundleAppId.empty()) { - const auto bundleAppIdBuffer = winrt::Windows::Security::Cryptography::CryptographicBuffer::ConvertStringToBinary( - winrt::to_hstring(bundleAppId), winrt::Windows::Security::Cryptography::BinaryStringEncoding::Utf16BE); - hash.Append(bundleAppIdBuffer); + const auto packageNameBuffer = winrt::Windows::Security::Cryptography::CryptographicBuffer::ConvertStringToBinary( + winrt::to_hstring(packageName), winrt::Windows::Security::Cryptography::BinaryStringEncoding::Utf16BE); + hash.Append(packageNameBuffer); } else { const auto processId = GetCurrentProcessId(); std::vector processIdBytes( @@ -203,6 +203,22 @@ std::string GetDeviceId(const std::string &bundleAppId) { return winrt::to_string(hashString); } +std::string GetPackageName(const std::string &bundleAppId) { + if (!bundleAppId.empty()) { + return bundleAppId; + } + + std::string packageName{"RNW"}; + wchar_t fullName[PACKAGE_FULL_NAME_MAX_LENGTH]{}; + UINT32 size = ARRAYSIZE(fullName); + if (SUCCEEDED(GetCurrentPackageFullName(&size, fullName))) { + // we are in an unpackaged app + packageName = winrt::to_string(fullName); + } + + return packageName; +} + std::future PollForLiveReload(const std::string &url) { winrt::Windows::Web::Http::HttpClient httpClient; winrt::Windows::Foundation::Uri uri(Microsoft::Common::Unicode::Utf8ToUtf16(url)); @@ -270,23 +286,28 @@ void DevSupportManager::StopPollingLiveReload() { m_cancellation_token = true; } +void DevSupportManager::OpenDevTools(const std::string &bundleAppId) { + winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter filter; + filter.CacheControl().ReadBehavior(winrt::Windows::Web::Http::Filters::HttpCacheReadBehavior::NoCache); + winrt::Windows::Web::Http::HttpClient httpClient(filter); + // TODO: Use currently configured dev server host + winrt::Windows::Foundation::Uri uri( + Microsoft::Common::Unicode::Utf8ToUtf16(facebook::react::DevServerHelper::get_OpenDebuggerUrl( + std::string{DevServerHelper::DefaultPackagerHost}, + DevServerHelper::DefaultPackagerPort, + GetDeviceId(GetPackageName(bundleAppId))))); + + winrt::Windows::Web::Http::HttpRequestMessage request(winrt::Windows::Web::Http::HttpMethod::Post(), uri); + httpClient.SendRequestAsync(request); +} + void DevSupportManager::EnsureHermesInspector( [[maybe_unused]] const std::string &packagerHost, [[maybe_unused]] const uint16_t packagerPort, [[maybe_unused]] const std::string &bundleAppId) noexcept { static std::once_flag once; std::call_once(once, [this, &packagerHost, packagerPort, &bundleAppId]() { - std::string packageName{bundleAppId}; - if (packageName.empty()) { - packageName = "RNW"; - wchar_t fullName[PACKAGE_FULL_NAME_MAX_LENGTH]{}; - UINT32 size = ARRAYSIZE(fullName); - if (SUCCEEDED(GetCurrentPackageFullName(&size, fullName))) { - // we are in an unpackaged app - packageName = winrt::to_string(fullName); - } - } - + std::string packageName = GetPackageName(bundleAppId); std::string deviceName("RNWHost"); auto hostNames = winrt::Windows::Networking::Connectivity::NetworkInformation::GetHostNames(); if (hostNames && hostNames.First() && hostNames.First().Current()) { diff --git a/vnext/Shared/DevSupportManager.h b/vnext/Shared/DevSupportManager.h index fcf1880cfd6..e5aa7bbab40 100644 --- a/vnext/Shared/DevSupportManager.h +++ b/vnext/Shared/DevSupportManager.h @@ -49,6 +49,7 @@ class DevSupportManager final : public facebook::react::IDevSupportManager { const uint16_t sourceBundlePort, std::function onChangeCallback) override; virtual void StopPollingLiveReload() override; + virtual void OpenDevTools(const std::string &bundleAppId) override; virtual void EnsureHermesInspector( const std::string &packagerHost, diff --git a/vnext/Shared/IDevSupportManager.h b/vnext/Shared/IDevSupportManager.h index 9f2fd6f53ae..3ce638e260f 100644 --- a/vnext/Shared/IDevSupportManager.h +++ b/vnext/Shared/IDevSupportManager.h @@ -24,6 +24,7 @@ struct IDevSupportManager { const uint16_t sourceBundlePort, std::function onChangeCallback) = 0; virtual void StopPollingLiveReload() = 0; + virtual void OpenDevTools(const std::string &bundleAppId) = 0; virtual void EnsureHermesInspector( const std::string &packagerHost,