From d8c7b48c2af1b8c231c07c00bb37b8ce3db96946 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 02:19:27 +0000 Subject: [PATCH 1/4] Initial plan From f759ac8cd90ebcf97a5c480c2006f25d0651f4ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 02:29:47 +0000 Subject: [PATCH 2/4] Add wil::winrt_event crash-resistant event handler Port the ResilientEvent from WidgetDeveloperPlatform as wil::winrt_event in cppwinrt_authoring.h. Adds three traits classes (swallow, propagate, failfast) and the winrt_event template that wraps handlers in try/catch at registration time to support configurable exception handling. Default behavior swallows exceptions so all handlers are invoked even if some throw. Tests added to CppWinRTAuthoringTests.cpp. Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com> --- _codeql_detected_source_root | 1 + include/wil/cppwinrt_authoring.h | 86 ++++++++++++++++++++++++++++ tests/CppWinRTAuthoringTests.cpp | 96 ++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 120000 _codeql_detected_source_root diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 000000000..945c9b46d --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/include/wil/cppwinrt_authoring.h b/include/wil/cppwinrt_authoring.h index 2ee4ad9c2..3ec3035da 100644 --- a/include/wil/cppwinrt_authoring.h +++ b/include/wil/cppwinrt_authoring.h @@ -251,6 +251,92 @@ struct typed_event : wil::details::event_base). + * @tparam Traits Traits class controlling what happens when a handler throws an exception. + * Defaults to swallow_event_errors_traits for crash-resistant behavior where + * all handlers are invoked regardless of whether earlier ones threw. + * @details Usage example: + * @code + * // Crash-resistant event (default): exceptions from handlers are swallowed + * wil::winrt_event> MyEvent; + * + * // Event that propagates exceptions (same behavior as winrt::event): + * wil::winrt_event, + * wil::propagate_event_errors_traits> MyStrictEvent; + * @endcode + */ +template +struct winrt_event +{ + winrt::event_token operator()(T const& handler) + { + return m_handler.add([handler](auto&&... args) { + try + { + handler(std::forward(args)...); + } + catch (...) + { + Traits::on_handler_exception(std::current_exception()); + } + }); + } + + void operator()(winrt::event_token const& token) noexcept + { + m_handler.remove(token); + } + + template + void invoke(TArgs&&... args) + { + m_handler(std::forward(args)...); + } + +private: + winrt::event m_handler; +}; + #endif // !defined(__WIL_CPPWINRT_AUTHORING_INCLUDED_FOUNDATION) && defined(WINRT_Windows_Foundation_H) #if (!defined(__WIL_CPPWINRT_AUTHORING_INCLUDED_XAML_DATA) && (defined(WINRT_Microsoft_UI_Xaml_Data_H) || defined(WINRT_Windows_UI_Xaml_Data_H))) || \ diff --git a/tests/CppWinRTAuthoringTests.cpp b/tests/CppWinRTAuthoringTests.cpp index ad4ebaab4..fc99033f6 100644 --- a/tests/CppWinRTAuthoringTests.cpp +++ b/tests/CppWinRTAuthoringTests.cpp @@ -266,6 +266,102 @@ TEST_CASE("CppWinRTAuthoringTests::EventsAndCppWinRt", "[property]") test.Closed(token); } +TEST_CASE("CppWinRTAuthoringTests::WinrtEvent", "[winrt_event]") +{ + // Basic smoke test: register, invoke, unregister + struct Test + { + wil::winrt_event> MyEvent; + } test; + + int value = 0; + auto token = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int args) { + value = args; + }); + test.MyEvent.invoke(nullptr, 42); + REQUIRE(value == 42); + test.MyEvent(token); +} + +TEST_CASE("CppWinRTAuthoringTests::WinrtEventSwallowExceptions", "[winrt_event]") +{ + // Default (swallow) traits: all handlers are called even if some throw + struct Test + { + wil::winrt_event> MyEvent; + } test; + + int callCount = 0; + + // First handler throws + auto token1 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int) { + ++callCount; + throw winrt::hresult_not_implemented{}; + }); + + // Second handler should still be called + auto token2 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int args) { + ++callCount; + REQUIRE(args == 42); + }); + + // invoke() must not throw even though first handler throws + REQUIRE_NOTHROW(test.MyEvent.invoke(nullptr, 42)); + + // Both handlers must have been called + REQUIRE(callCount == 2); + + test.MyEvent(token1); + test.MyEvent(token2); +} + +TEST_CASE("CppWinRTAuthoringTests::WinrtEventPropagateExceptions", "[winrt_event]") +{ + // Propagate traits: first exception propagates and remaining handlers are skipped + struct Test + { + wil::winrt_event, wil::propagate_event_errors_traits> MyEvent; + } test; + + int callCount = 0; + + auto token1 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int) { + ++callCount; + throw winrt::hresult_not_implemented{}; + }); + + // This handler should NOT be called because the first one throws + auto token2 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int) { + ++callCount; + }); + + // invoke() must throw + REQUIRE_THROWS_AS(test.MyEvent.invoke(nullptr, 42), winrt::hresult_not_implemented); + + // Only the first handler ran + REQUIRE(callCount == 1); + + test.MyEvent(token1); + test.MyEvent(token2); +} + +TEST_CASE("CppWinRTAuthoringTests::WinrtEventTypedDelegate", "[winrt_event]") +{ + // winrt_event works with TypedEventHandler delegates + struct Test + { + wil::winrt_event> MyEvent; + } test; + + int value = 0; + auto token = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int args) { + value = args; + }); + test.MyEvent.invoke(nullptr, 99); + REQUIRE(value == 99); + test.MyEvent(token); +} + #include TEST_CASE("CppWinRTAuthoringTests::NotifyPropertyChanged", "[property]") From 75eb8d968ccfcd88f345c870fd4db0333c098c80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 02:30:02 +0000 Subject: [PATCH 3/4] Add _codeql_detected_source_root to .gitignore Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com> --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3973a16bf..699ac84ee 100644 --- a/.gitignore +++ b/.gitignore @@ -334,3 +334,4 @@ ASALocalRun/ # CMake/Build output build/ +_codeql_detected_source_root From 808b46010561e4da978393b49995d89ff3915c5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:15:17 +0000 Subject: [PATCH 4/4] Improve winrt_event: LOG_CAUGHT_EXCEPTION by default, rpc_resilient_event_errors_traits, handler removal support, better tests Co-authored-by: jonwis <18537118+jonwis@users.noreply.github.com> --- include/wil/cppwinrt_authoring.h | 128 +++++++++++++++++++++---- tests/CppWinRTAuthoringTests.cpp | 156 ++++++++++++++++++++++++++++++- 2 files changed, 265 insertions(+), 19 deletions(-) diff --git a/include/wil/cppwinrt_authoring.h b/include/wil/cppwinrt_authoring.h index 3ec3035da..3eba71de2 100644 --- a/include/wil/cppwinrt_authoring.h +++ b/include/wil/cppwinrt_authoring.h @@ -251,16 +251,40 @@ struct typed_event : wil::details::event_base(HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE)) + || hr == RPC_E_SERVER_DIED + || hr == RPC_E_SERVER_DIED_DNE + || hr == RPC_E_CALL_CANCELED; + // clang-format on + } +} // namespace details +/// @endcond + /** - * @brief Traits for wil::winrt_event that swallows exceptions thrown by event handlers. - * All handlers are called even if some throw; exceptions are discarded. - * This provides crash-resistant behavior and is the default for wil::winrt_event. + * @brief Traits for wil::winrt_event that logs and swallows exceptions thrown by event handlers. + * All handlers are called even if some throw; exceptions are logged via LOG_CAUGHT_EXCEPTION() + * and then discarded, allowing remaining handlers to run. This is the default for wil::winrt_event. + * + * @returns false (never removes the handler). + * + * @note Requires wil/result.h or wil/result_macros.h to be included for LOG_CAUGHT_EXCEPTION(). + * To suppress logging, implement custom traits with an empty on_handler_exception. */ struct swallow_event_errors_traits { - static void on_handler_exception(std::exception_ptr) noexcept + static bool on_handler_exception(std::exception_ptr) noexcept { - // Intentionally discard the exception to allow remaining handlers to run. + LOG_CAUGHT_EXCEPTION(); + return false; // Keep the handler for future invocations } }; @@ -268,10 +292,11 @@ struct swallow_event_errors_traits * @brief Traits for wil::winrt_event that propagates exceptions thrown by event handlers. * If a handler throws, the exception propagates out of invoke() and remaining handlers * are not called. This matches the behavior of winrt::event. + * @note This method never returns; it always rethrows the passed exception. */ struct propagate_event_errors_traits { - static void on_handler_exception(std::exception_ptr ep) + [[noreturn]] static bool on_handler_exception(std::exception_ptr ep) { std::rethrow_exception(ep); } @@ -283,9 +308,47 @@ struct propagate_event_errors_traits */ struct failfast_event_errors_traits { - [[noreturn]] static void on_handler_exception(std::exception_ptr) noexcept + [[noreturn]] static bool on_handler_exception(std::exception_ptr) noexcept { - FAIL_FAST(); + FAIL_FAST_CAUGHT_EXCEPTION(); + } +}; + +/** + * @brief Traits for wil::winrt_event that handles RPC/IPC disconnection errors resiliently. + * When a handler throws an RPC disconnection error (e.g., RPC_E_DISCONNECTED, + * RPC_S_SERVER_UNAVAILABLE, etc.), the handler is removed from the event so it will not + * be called in future invocations. All exceptions (both RPC and non-RPC) are logged via + * LOG_CAUGHT_EXCEPTION(). + * + * This is useful when handlers may be in remote processes that can disappear at any time. + * + * @returns true for RPC disconnection errors (remove the broken handler), + * false for all other errors (keep the handler). + */ +struct rpc_resilient_event_errors_traits +{ + static bool on_handler_exception(std::exception_ptr ep) noexcept + { + try + { + std::rethrow_exception(ep); + } + catch (winrt::hresult_error const& e) + { + if (details::is_rpc_disconnection_error(e.code())) + { + LOG_CAUGHT_EXCEPTION_MSG("Removing disconnected WinRT event handler"); + return true; // Remove the broken handler + } + LOG_CAUGHT_EXCEPTION(); + return false; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + return false; + } } }; @@ -293,16 +356,30 @@ struct failfast_event_errors_traits * @brief A WinRT event with configurable exception handling for individual handlers. * @tparam T The delegate type (e.g., winrt::Windows::Foundation::TypedEventHandler). * @tparam Traits Traits class controlling what happens when a handler throws an exception. - * Defaults to swallow_event_errors_traits for crash-resistant behavior where - * all handlers are invoked regardless of whether earlier ones threw. - * @details Usage example: + * The Traits::on_handler_exception static method is called with std::current_exception() + * and must return bool: true to remove the handler from future invocations, false to keep it. + * Defaults to swallow_event_errors_traits, which logs and swallows all exceptions. + * @details Thread safety is inherited from winrt::event. + * Handlers that throw and are marked for removal (by returning true from on_handler_exception) + * will not receive any further invocations after the current one completes. + * Usage example: * @code - * // Crash-resistant event (default): exceptions from handlers are swallowed + * // Crash-resistant (default): exceptions are logged and swallowed; all handlers are invoked * wil::winrt_event> MyEvent; * - * // Event that propagates exceptions (same behavior as winrt::event): + * // RPC-resilient: disconnected remote handlers are automatically removed + * wil::winrt_event, + * wil::rpc_resilient_event_errors_traits> MyRpcEvent; + * + * // Same behavior as winrt::event - first exception propagates, remaining handlers skipped: * wil::winrt_event, * wil::propagate_event_errors_traits> MyStrictEvent; + * + * // Custom traits for telemetry, logging, etc.: + * struct my_telemetry_traits { + * static bool on_handler_exception(std::exception_ptr ep) noexcept { ... return false; } + * }; + * wil::winrt_event, my_telemetry_traits> MyEvent; * @endcode */ template @@ -310,31 +387,46 @@ struct winrt_event { winrt::event_token operator()(T const& handler) { - return m_handler.add([handler](auto&&... args) { + // The lambda needs both the event (to call remove) and its own token. + // Token is only available after add() returns, so we store it in a shared slot + // filled in after registration. We hold a weak_ptr to the event to safely handle + // the case where winrt_event is destroyed between invocations. + auto tokenSlot = std::make_shared(); + std::weak_ptr> weakEvent{m_event}; + winrt::event_token token = m_event->add([handler, tokenSlot, weakEvent](auto&&... args) { try { handler(std::forward(args)...); } catch (...) { - Traits::on_handler_exception(std::current_exception()); + if (Traits::on_handler_exception(std::current_exception())) + { + if (auto e = weakEvent.lock()) + { + e->remove(*tokenSlot); + } + } } }); + *tokenSlot = token; + return token; } void operator()(winrt::event_token const& token) noexcept { - m_handler.remove(token); + m_event->remove(token); } template void invoke(TArgs&&... args) { - m_handler(std::forward(args)...); + (*m_event)(std::forward(args)...); } private: - winrt::event m_handler; + // shared_ptr enables the handler lambdas to hold a weak_ptr for safe self-removal + std::shared_ptr> m_event = std::make_shared>(); }; #endif // !defined(__WIL_CPPWINRT_AUTHORING_INCLUDED_FOUNDATION) && defined(WINRT_Windows_Foundation_H) diff --git a/tests/CppWinRTAuthoringTests.cpp b/tests/CppWinRTAuthoringTests.cpp index fc99033f6..436b2fcb2 100644 --- a/tests/CppWinRTAuthoringTests.cpp +++ b/tests/CppWinRTAuthoringTests.cpp @@ -13,6 +13,40 @@ #include #include +#if defined(WIL_ENABLE_EXCEPTIONS) +// File-scope traits used by WinrtEventCustomTraits; counters are thread_local to avoid +// inter-test interference when tests run on the same thread sequentially. +namespace +{ +thread_local int g_counting_swallowed = 0; +thread_local int g_counting_removed = 0; + +struct counting_event_errors_traits +{ + static bool on_handler_exception(std::exception_ptr ep) noexcept + { + try + { + std::rethrow_exception(ep); + } + catch (winrt::hresult_error const& e) + { + if (e.code() == E_ABORT) + { + ++g_counting_removed; + return true; // Remove handler + } + } + catch (...) + { + } + ++g_counting_swallowed; + return false; // Keep handler + } +}; +} // namespace +#endif + struct my_async_status : winrt::implements { wil::single_threaded_property Status{winrt::Windows::Foundation::AsyncStatus::Started}; @@ -283,14 +317,16 @@ TEST_CASE("CppWinRTAuthoringTests::WinrtEvent", "[winrt_event]") test.MyEvent(token); } +#if defined(WIL_ENABLE_EXCEPTIONS) TEST_CASE("CppWinRTAuthoringTests::WinrtEventSwallowExceptions", "[winrt_event]") { - // Default (swallow) traits: all handlers are called even if some throw + // Default (swallow) traits: all handlers are called even if some throw, exceptions are logged struct Test { wil::winrt_event> MyEvent; } test; + witest::TestFailureCache failures; int callCount = 0; // First handler throws @@ -311,6 +347,16 @@ TEST_CASE("CppWinRTAuthoringTests::WinrtEventSwallowExceptions", "[winrt_event]" // Both handlers must have been called REQUIRE(callCount == 2); + // Exception must have been logged (caught, not leaked) + REQUIRE(failures.size() == 1); + REQUIRE(failures[0].hr == E_NOTIMPL); + + // Handlers are kept after a non-removal exception + callCount = 0; + failures.clear(); + REQUIRE_NOTHROW(test.MyEvent.invoke(nullptr, 42)); + REQUIRE(callCount == 2); // Both still registered + test.MyEvent(token1); test.MyEvent(token2); } @@ -362,6 +408,114 @@ TEST_CASE("CppWinRTAuthoringTests::WinrtEventTypedDelegate", "[winrt_event]") test.MyEvent(token); } +TEST_CASE("CppWinRTAuthoringTests::WinrtEventRpcResilienceRemovesBrokenHandler", "[winrt_event]") +{ + // rpc_resilient_event_errors_traits: RPC disconnection errors cause handler removal + struct Test + { + wil::winrt_event, wil::rpc_resilient_event_errors_traits> MyEvent; + } test; + + witest::TestFailureCache failures; + int callCount = 0; + + // Handler that throws RPC_E_DISCONNECTED (simulates a broken remote subscriber) + auto token1 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int) { + ++callCount; + throw winrt::hresult_error{RPC_E_DISCONNECTED}; + }); + + // A healthy handler that should always be called + auto token2 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int) { + ++callCount; + }); + + // First invoke: both handlers run; broken one throws but is caught, not leaked + REQUIRE_NOTHROW(test.MyEvent.invoke(nullptr, 42)); + REQUIRE(callCount == 2); + REQUIRE(!failures.empty()); // RPC error was logged + + failures.clear(); + callCount = 0; + + // Second invoke: broken handler was removed; only token2 runs + REQUIRE_NOTHROW(test.MyEvent.invoke(nullptr, 42)); + REQUIRE(callCount == 1); // Only healthy handler called + REQUIRE(failures.empty()); // No more errors + + test.MyEvent(token2); + // token1 was already removed by the resilience mechanism; removing again is a no-op + test.MyEvent(token1); +} + +TEST_CASE("CppWinRTAuthoringTests::WinrtEventRpcResilienceKeepsHandlerOnNonRpcError", "[winrt_event]") +{ + // rpc_resilient_event_errors_traits: non-RPC errors are logged but handler is kept + struct Test + { + wil::winrt_event, wil::rpc_resilient_event_errors_traits> MyEvent; + } test; + + witest::TestFailureCache failures; + int callCount = 0; + + // Handler that throws a non-RPC error + auto token1 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int) { + ++callCount; + throw winrt::hresult_not_implemented{}; + }); + + // First invoke: exception caught, logged, handler kept + REQUIRE_NOTHROW(test.MyEvent.invoke(nullptr, 42)); + REQUIRE(callCount == 1); + REQUIRE(failures.size() == 1); + REQUIRE(failures[0].hr == E_NOTIMPL); + + failures.clear(); + callCount = 0; + + // Second invoke: handler was NOT removed (not an RPC error) + REQUIRE_NOTHROW(test.MyEvent.invoke(nullptr, 42)); + REQUIRE(callCount == 1); // Handler still registered + REQUIRE(failures.size() == 1); + + test.MyEvent(token1); +} + +TEST_CASE("CppWinRTAuthoringTests::WinrtEventCustomTraits", "[winrt_event]") +{ + // Custom traits: verify the trait interface (on_handler_exception returning bool) + // counting_event_errors_traits: removes handlers that throw E_ABORT; swallows everything else + g_counting_swallowed = 0; + g_counting_removed = 0; + + struct Test + { + wil::winrt_event, counting_event_errors_traits> MyEvent; + } test; + + // Handler that throws E_ABORT → should be removed + auto token1 = test.MyEvent([](const winrt::Windows::Foundation::IInspectable&, int) { + throw winrt::hresult_error{E_ABORT}; + }); + // Handler that throws something else → should be kept + auto token2 = test.MyEvent([](const winrt::Windows::Foundation::IInspectable&, int) { + throw winrt::hresult_error{E_FAIL}; + }); + + REQUIRE_NOTHROW(test.MyEvent.invoke(nullptr, 1)); + REQUIRE(g_counting_removed == 1); + REQUIRE(g_counting_swallowed == 1); + + // Second invoke: token1 was removed, only token2 runs + REQUIRE_NOTHROW(test.MyEvent.invoke(nullptr, 2)); + REQUIRE(g_counting_removed == 1); // No new removals + REQUIRE(g_counting_swallowed == 2); // One more swallow from token2 + + test.MyEvent(token2); +} +#endif // WIL_ENABLE_EXCEPTIONS + #include TEST_CASE("CppWinRTAuthoringTests::NotifyPropertyChanged", "[property]")