From 3cdad27d02a18a86ad33a7bf8f604911d3efb95a Mon Sep 17 00:00:00 2001 From: rahulsutariya Date: Thu, 21 May 2026 17:35:07 +0530 Subject: [PATCH] Extend "GenericSkeleton" to open non-owned shared memory objects. Issue: https://github.com/eclipse-score/communication/issues/387 --- score/mw/com/impl/bindings/lola/skeleton.cpp | 215 ++++++++++++----- score/mw/com/impl/bindings/lola/skeleton.h | 48 +++- .../com/impl/bindings/lola/skeleton_test.cpp | 223 ++++++++++++++++++ 3 files changed, 422 insertions(+), 64 deletions(-) diff --git a/score/mw/com/impl/bindings/lola/skeleton.cpp b/score/mw/com/impl/bindings/lola/skeleton.cpp index 403609a37..c6e872474 100644 --- a/score/mw/com/impl/bindings/lola/skeleton.cpp +++ b/score/mw/com/impl/bindings/lola/skeleton.cpp @@ -142,6 +142,49 @@ T& DereferenceWithNullCheck(std::unique_ptr& ptr) } // namespace +Skeleton::ShmReuseStrategy Skeleton::DetermineShmReuseStrategy( + const bool previous_shm_region_unused_by_proxies) const noexcept +{ + // In case of inter-VM forwarded services, always open existing SHM created by the gateway on the other VM + if (lola_service_instance_deployment_.inter_vm_support_ && lola_service_instance_deployment_.inter_vm_forwarded_) + { + return ShmReuseStrategy::kOpenGatewayForwardedShm; + } + + // If we can exclusively lock the usage marker file, the previous SHM region is not being used by proxies. + // This means either: + // (1) Previous skeleton stopped cleanly with no proxies, or + // (2) Previous skeleton crashed + // In both cases, we recreate the SHM to ensure a clean state. + if (previous_shm_region_unused_by_proxies) + { + return ShmReuseStrategy::kRecreateShm; + } + + // If we cannot lock the usage marker file, proxies are still using the previous SHM region. + // This means the previous skeleton crashed while proxies were subscribed, so we reuse the existing SHM. + return ShmReuseStrategy::kReuseExistingShm; +} + +Skeleton::ShmRemovalStrategy Skeleton::DetermineShmRemovalStrategy( + const bool can_exclusively_lock_usage_file) const noexcept +{ + // If we cannot lock the usage marker file that means that the proxies are still subscribed and using the SHM + if (!can_exclusively_lock_usage_file) + { + return ShmRemovalStrategy::kRemoveNeitherShmNorMarkerFile; + } + + // Gateway-forwarded services don't own the SHM (it's on the other VM), so only remove the marker file + if (IsGatewayForwardedSkeleton()) + { + return ShmRemovalStrategy::kRemoveMarkerFileOnly; + } + + // Regular skeleton: we own the SHM, so remove both SHM and marker file + return ShmRemovalStrategy::kRemoveShmAndMarkerFile; +} + // Suppress "AUTOSAR C++14 A15-5-3" rule findings. This rule states: "The std::terminate() function shall not be // called implicitly". std::terminate() is implicitly called from 'service_instance_existence_marker_file.value()' // in case the 'service_instance_existence_marker_file' doesn't have value but as we check before with 'has_value()' @@ -210,6 +253,7 @@ Skeleton::Skeleton(const InstanceIdentifier& identifier, quality_type_{InstanceIdentifierView{identifier_}.GetServiceInstanceDeployment().asilLevel_}, lola_instance_id_{lola_service_instance_deployment.instance_id_.value().GetId()}, lola_service_id_{lola_service_type_deployment.service_id_}, + lola_service_instance_deployment_{lola_service_instance_deployment}, shm_path_builder_{std::move(shm_path_builder)}, memory_manager_{GetInstanceQualityType(), DereferenceWithNullCheck(shm_path_builder_), @@ -227,6 +271,7 @@ Skeleton::Skeleton(const InstanceIdentifier& identifier, method_subscription_registration_guard_qm_{}, method_subscription_registration_guard_asil_b_{}, was_old_shm_region_reopened_{false}, + use_gateway_forwarded_shm_{false}, filesystem_{std::move(filesystem)}, prepare_offer_called_{false}, prepare_stop_offer_called_{false}, @@ -258,43 +303,69 @@ auto Skeleton::PrepareOffer(SkeletonEventBindings& events, std::unique_lock service_instance_usage_lock{service_instance_usage_mutex, std::defer_lock}; const bool previous_shm_region_unused_by_proxies = service_instance_usage_lock.try_lock(); - was_old_shm_region_reopened_ = !previous_shm_region_unused_by_proxies; - if (previous_shm_region_unused_by_proxies) - { + const auto strategy = DetermineShmReuseStrategy(previous_shm_region_unused_by_proxies); - score::mw::log::LogDebug("lola") << "Recreating SHM of Skeleton (S:" << lola_service_id_ - << "I:" << lola_instance_id_ << ")"; - // Since the previous shared memory region is not being currently used by proxies, this can mean 2 things: - // (1) The previous shared memory was properly created and OfferService finished (the SkeletonBinding and - // all Skeleton service elements finished their PrepareOffer calls) and either no Proxies subscribed or they - // have all since unsubscribed. Or, (2), the previous Skeleton crashed while setting up the shared memory. - // Since we don't differentiate between the 2 cases and because it's unused anyway, we simply remove the old - // memory region and re-create it. - memory_manager_.RemoveStaleSharedMemoryArtefacts(); - - const auto create_result = - memory_manager_.CreateSharedMemory(events, fields, std::move(register_shm_object_trace_callback)); - if (!(create_result.has_value())) + Result shm_setup_result{}; + switch (strategy) + { + case ShmReuseStrategy::kOpenGatewayForwardedShm: { - score::mw::log::LogError("lola") << "Could not create shared memory region for Skeleton."; - return create_result; + score::mw::log::LogDebug("lola") << "Using SHM of Skeleton (S:" << lola_service_id_ + << "I:" << lola_instance_id_ << ") for gateway-forwarded service"; + shm_setup_result = memory_manager_.OpenExistingSharedMemory(std::move(register_shm_object_trace_callback)); + if (!shm_setup_result.has_value()) + { + score::mw::log::LogError("lola") + << "Could not open existing shared memory region for gateway-forwarded service."; + return MakeUnexpected(ComErrc::kBindingFailure, + "Could not open existing shared memory region for gateway-forwarded service."); + } + was_old_shm_region_reopened_ = false; + use_gateway_forwarded_shm_ = true; + break; } + + case ShmReuseStrategy::kRecreateShm: + score::mw::log::LogDebug("lola") + << "Recreating SHM of Skeleton (S:" << lola_service_id_ << "I:" << lola_instance_id_ << ")"; + memory_manager_.RemoveStaleSharedMemoryArtefacts(); + shm_setup_result = + memory_manager_.CreateSharedMemory(events, fields, std::move(register_shm_object_trace_callback)); + if (!(shm_setup_result.has_value())) + { + score::mw::log::LogError("lola") << "Could not create shared memory region for Skeleton."; + } + was_old_shm_region_reopened_ = false; + use_gateway_forwarded_shm_ = false; + break; + + case ShmReuseStrategy::kReuseExistingShm: + score::mw::log::LogDebug("lola") + << "Reusing SHM of Skeleton (S:" << lola_service_id_ << "I:" << lola_instance_id_ << ")"; + shm_setup_result = memory_manager_.OpenExistingSharedMemory(std::move(register_shm_object_trace_callback)); + if (!shm_setup_result.has_value()) + { + score::mw::log::LogError("lola") << "Could not open existing shared memory region for Skeleton."; + } + else + { + memory_manager_.CleanupSharedMemoryAfterCrash(); + } + was_old_shm_region_reopened_ = true; + use_gateway_forwarded_shm_ = false; + break; + + // LCOV_EXCL_START (DetermineShmReuseStrategy always returns a valid strategy; kUnknownStrategy is unreachable) + case ShmReuseStrategy::kUnknownStrategy: + default: + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(false, "Unknown SHM reuse strategy."); + break; + // LCOV_EXCL_STOP } - else + + if (!shm_setup_result.has_value()) { - score::mw::log::LogDebug("lola") << "Reusing SHM of Skeleton (S:" << lola_service_id_ - << "I:" << lola_instance_id_ << ")"; - // Since the previous shared memory region is being currently used by proxies, it must have been properly - // created and OfferService finished. Therefore, we can simply re-open it and cleanup any previous - // in-writing transactions by the previous skeleton. - const auto open_result = - memory_manager_.OpenExistingSharedMemory(std::move(register_shm_object_trace_callback)); - if (!open_result.has_value()) - { - score::mw::log::LogError("lola") << "Could not open existing shared memory region for Skeleton."; - return open_result; - } - memory_manager_.CleanupSharedMemoryAfterCrash(); + return shm_setup_result; } // If there are no registered SkeletonMethods, then we don't need to register a method subscribed handler and @@ -422,27 +493,42 @@ auto Skeleton::PrepareStopOffer(std::optional memory::shared::ExclusiveFlockMutex service_instance_usage_mutex{*service_instance_usage_marker_file_}; std::unique_lock service_instance_usage_lock{service_instance_usage_mutex, std::defer_lock}; - if (!service_instance_usage_lock.try_lock()) + const bool can_exclusively_lock_usage_file = service_instance_usage_lock.try_lock(); + const auto strategy = DetermineShmRemovalStrategy(can_exclusively_lock_usage_file); + switch (strategy) { - score::mw::log::LogInfo("lola") - << "Skeleton::RemoveSharedMemory(): Could not exclusively lock service instance usage " - "marker file indicating that some proxies are still subscribed. Will not remove shared memory and " - "will " - "keep the service-instance-usage-marker-file for future skeletons to reuse."; - return; - } - else - { - // Since we were able to exclusively lock the usage marker file, it means that no proxies are currently - // using the shared memory region. - memory_manager_.RemoveSharedMemory(); - // We take ownership of the usage marker file so that it gets unlinked from the fs, when we destroy it. - service_instance_usage_marker_file_.value().TakeOwnership(); - // Unlock the usage marker file before destroying it so that the destructor doesn't try to unlock an already - // invalid file/fd. - service_instance_usage_lock.unlock(); - // this effectively deletes the usage marker file from filesystem - service_instance_usage_marker_file_.reset(); + case ShmRemovalStrategy::kRemoveNeitherShmNorMarkerFile: + score::mw::log::LogInfo("lola") + << "Skeleton::PrepareStopOffer(): Could not exclusively lock service instance usage " + "marker file indicating that some proxies are still subscribed. Will not remove shared memory."; + return; + + case ShmRemovalStrategy::kRemoveMarkerFileOnly: + score::mw::log::LogDebug("lola") + << "Skeleton::PrepareStopOffer(): Gateway-forwarded service - removing marker file only (S:" + << lola_service_id_ << " I:" << lola_instance_id_ << ")"; + service_instance_usage_marker_file_.value().TakeOwnership(); + service_instance_usage_lock.unlock(); + service_instance_usage_marker_file_.reset(); + break; + + case ShmRemovalStrategy::kRemoveShmAndMarkerFile: + score::mw::log::LogDebug("lola") + << "Skeleton::PrepareStopOffer(): Removing SHM and marker file (S:" << lola_service_id_ + << " I:" << lola_instance_id_ << ")"; + memory_manager_.RemoveSharedMemory(); + service_instance_usage_marker_file_.value().TakeOwnership(); + service_instance_usage_lock.unlock(); + service_instance_usage_marker_file_.reset(); + break; + + // LCOV_EXCL_START (DetermineShmRemovalStrategy always returns a valid strategy; kUnknownStrategy is + // unreachable) + case ShmRemovalStrategy::kUnknownStrategy: + default: + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(false, "Unknown SHM removal strategy."); + break; + // LCOV_EXCL_STOP } memory_manager_.Reset(); @@ -515,22 +601,25 @@ auto Skeleton::RegisterGeneric(const ElementFqId element_fq_id, const size_t sample_size, const size_t sample_alignment) noexcept -> GenericRegistrationResult { - if (was_old_shm_region_reopened_) + if (use_gateway_forwarded_shm_ || was_old_shm_region_reopened_) { auto [event_data_control_qm, event_data_control_asil_b] = memory_manager_.RetrieveEventControlsFromOpenedSharedMemory(element_fq_id); - // We can have transactions in the TransactionLogs relating to tracing (QM only) or field getter logic (QM and / - // or ASIL-B). We try rolling back all TransactionLogSets which are found. - // We rollback any transactions in the TransactionLog that correspond to the SkeletonEvent even if - // tracing is disabled in the current process. It's possible that we could have tracing disabled in this process - // but the crashed process had tracing enabled and therefore may have transactions that need to be rolled back. - // If tracing was also disabled in the previous process or if there are no transactions to rollback, - // RollbackSkeletonTracingTransactions will simply do nothing. - memory_manager_.RollbackSkeletonTracingTransactions(event_data_control_qm); - if (event_data_control_asil_b != nullptr) + if (was_old_shm_region_reopened_) { - memory_manager_.RollbackSkeletonTracingTransactions(*event_data_control_asil_b); + // We can have transactions in the TransactionLogs relating to tracing (QM only) or field getter logic (QM + // and / or ASIL-B). We try rolling back all TransactionLogSets which are found. + // We rollback any transactions in the TransactionLog that correspond to the SkeletonEvent even if + // tracing is disabled in the current process. It's possible that we could have tracing disabled in this + // process but the crashed process had tracing enabled and therefore may have transactions that need to be + // rolled back. If tracing was also disabled in the previous process or if there are no transactions to + // rollback, RollbackSkeletonTracingTransactions will simply do nothing. + memory_manager_.RollbackSkeletonTracingTransactions(event_data_control_qm); + if (event_data_control_asil_b != nullptr) + { + memory_manager_.RollbackSkeletonTracingTransactions(*event_data_control_asil_b); + } } auto& event_data_storage = memory_manager_.RetrieveEventDataFromOpenedSharedMemory(element_fq_id); diff --git a/score/mw/com/impl/bindings/lola/skeleton.h b/score/mw/com/impl/bindings/lola/skeleton.h index b0539bd59..a84b0bd9d 100644 --- a/score/mw/com/impl/bindings/lola/skeleton.h +++ b/score/mw/com/impl/bindings/lola/skeleton.h @@ -170,6 +170,42 @@ class Skeleton final : public SkeletonBinding bool VerifyAllMethodsRegistered() const override; private: + /// \brief Strategies for handling shared memory during PrepareOffer + enum class ShmReuseStrategy : std::uint8_t + { + kUnknownStrategy = 0U, + kOpenGatewayForwardedShm, + kRecreateShm, + kReuseExistingShm + }; + + /// \brief Strategies for handling shared memory during PrepareStopOffer + enum class ShmRemovalStrategy : std::uint8_t + { + kUnknownStrategy = 0U, + kRemoveNeitherShmNorMarkerFile, + kRemoveMarkerFileOnly, + kRemoveShmAndMarkerFile + }; + + /// \brief Determines which shared memory reuse strategy to apply + /// \param previous_shm_region_unused_by_proxies Whether we could exclusively lock the usage marker file + /// \return The appropriate SHM reuse strategy + ShmReuseStrategy DetermineShmReuseStrategy(bool previous_shm_region_unused_by_proxies) const noexcept; + + /// \brief Determines which shared memory removal strategy to apply during StopOffer + /// \param can_exclusively_lock_usage_file Whether we could exclusively lock the usage marker file + /// \return The appropriate SHM removal strategy + ShmRemovalStrategy DetermineShmRemovalStrategy(bool can_exclusively_lock_usage_file) const noexcept; + + /// \brief Checks if this skeleton is forwarding a service from another VM via gateway + /// \return true if this skeleton mirrors SHM from another VM (read-only access), false if it owns the SHM + /// (read-write) + bool IsGatewayForwardedSkeleton() const noexcept + { + return lola_service_instance_deployment_.inter_vm_forwarded_; + } + Result OnServiceMethodsSubscribed(const ProxyInstanceIdentifier& proxy_instance_identifier, const uid_t proxy_uid, const QualityType asil_level, @@ -195,6 +231,7 @@ class Skeleton final : public SkeletonBinding QualityType quality_type_; LolaServiceInstanceId::InstanceId lola_instance_id_; LolaServiceTypeDeployment::ServiceId lola_service_id_; + const LolaServiceInstanceDeployment& lola_service_instance_deployment_; std::unique_ptr shm_path_builder_; @@ -226,6 +263,7 @@ class Skeleton final : public SkeletonBinding std::optional method_subscription_registration_guard_asil_b_; bool was_old_shm_region_reopened_; + bool use_gateway_forwarded_shm_; score::filesystem::Filesystem filesystem_; @@ -252,8 +290,16 @@ auto Skeleton::Register(const ElementFqId element_fq_id, SkeletonEventProperties -> RegistrationResult { // If the skeleton previously crashed and there are active proxies connected to the old shared memory, then we - // re-open that shared memory in PrepareOffer(). In that case, we should retrieved the EventDataControl and + // re-open that shared memory in PrepareOffer(). In that case, we should retrieve the EventDataControl and // EventDataStorage from the shared memory and attempt to rollback the Skeleton tracing transaction log. + // use_gateway_forwarded_shm_ is only ever set in inter-VM gateway setups, where a GenericSkeleton + // (type-erased) is used exclusively. A GenericSkeleton registers events via RegisterGeneric(), NOT + // via this typed template. Therefore reaching this function with use_gateway_forwarded_shm_ == true + // is a programming error: a typed Skeleton must never be used in a gateway-forwarding context. + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(!use_gateway_forwarded_shm_, + "Register must not be called for a gateway-forwarded " + "skeleton — use GenericSkeleton/RegisterGeneric instead"); + if (was_old_shm_region_reopened_) { auto [event_data_control_qm, event_data_control_asil_b] = diff --git a/score/mw/com/impl/bindings/lola/skeleton_test.cpp b/score/mw/com/impl/bindings/lola/skeleton_test.cpp index 2b8d8e2ec..90f3aaf91 100644 --- a/score/mw/com/impl/bindings/lola/skeleton_test.cpp +++ b/score/mw/com/impl/bindings/lola/skeleton_test.cpp @@ -516,6 +516,52 @@ TEST_F(SkeletonPrepareOfferFixture, PrepareOfferWillCallRegisterShmObjectTraceCa skeleton_->PrepareOffer(events_, fields_, register_shm_object_trace_callback.AsStdFunction()).has_value()); } +TEST_F(SkeletonPrepareOfferFixture, + PrepareOfferOpensExistingSharedMemoryForGatewayForwardedServiceWhenUsageFileCanBeLocked) +{ + auto service_instance_deployment = test::kValidMinimalAsilInstanceDeployment; + auto* const lola_service_instance_deployment = + std::get_if(&service_instance_deployment.bindingInfo_); + ASSERT_NE(lola_service_instance_deployment, nullptr); + lola_service_instance_deployment->inter_vm_support_ = true; + lola_service_instance_deployment->inter_vm_forwarded_ = true; + + const auto instance_identifier = + make_InstanceIdentifier(service_instance_deployment, test::kValidMinimalTypeDeployment); + InitialiseSkeleton(instance_identifier).WithNoConnectedProxy(); + + EXPECT_CALL(shared_memory_factory_mock_, RemoveStaleArtefacts(_)).Times(0); + EXPECT_CALL(shared_memory_factory_mock_, Create(test::kControlChannelPathQm, _, _, _, _)).Times(0); + EXPECT_CALL(shared_memory_factory_mock_, Create(test::kControlChannelPathAsilB, _, _, _, _)).Times(0); + EXPECT_CALL(shared_memory_factory_mock_, Create(test::kDataChannelPath, _, _, _, _)).Times(0); + + EXPECT_CALL(shared_memory_factory_mock_, Open(test::kControlChannelPathQm, true, _)); + EXPECT_CALL(shared_memory_factory_mock_, Open(test::kControlChannelPathAsilB, true, _)); + EXPECT_CALL(shared_memory_factory_mock_, Open(test::kDataChannelPath, true, _)) + .WillOnce(Return(data_shared_memory_resource_mock_)); + + EXPECT_TRUE(skeleton_->PrepareOffer(events_, fields_, std::move(kEmptyRegisterShmObjectTraceCallback)).has_value()); +} + +TEST_F(SkeletonPrepareOfferFixture, PrepareOfferFailsForGatewayForwardedServiceWhenOpeningSharedMemoryFails) +{ + auto service_instance_deployment = test::kValidMinimalAsilInstanceDeployment; + auto* const lola_service_instance_deployment = + std::get_if(&service_instance_deployment.bindingInfo_); + ASSERT_NE(lola_service_instance_deployment, nullptr); + lola_service_instance_deployment->inter_vm_support_ = true; + lola_service_instance_deployment->inter_vm_forwarded_ = true; + + const auto instance_identifier = + make_InstanceIdentifier(service_instance_deployment, test::kValidMinimalTypeDeployment); + InitialiseSkeleton(instance_identifier).WithNoConnectedProxy(); + + EXPECT_CALL(shared_memory_factory_mock_, Open(test::kControlChannelPathQm, true, _)).WillOnce(Return(nullptr)); + + EXPECT_FALSE( + skeleton_->PrepareOffer(events_, fields_, std::move(kEmptyRegisterShmObjectTraceCallback)).has_value()); +} + using SkeletonPrepareOfferDeathTest = SkeletonPrepareOfferFixture; TEST_F(SkeletonPrepareOfferDeathTest, CallingPrepareOfferWhenLolaRuntimeCannotBeAccessedTerminates) { @@ -706,6 +752,46 @@ TEST_F(SkeletonPrepareStopOfferFixture, PrepareStopOfferDoesNotRemoveUsageMarker EXPECT_FALSE(*was_usage_marker_file_closed); } +TEST_F(SkeletonPrepareStopOfferFixture, PrepareStopOfferForGatewayForwardedServiceRemovesOnlyUsageMarkerFile) +{ + bool was_usage_marker_file_closed{false}; + + auto service_instance_deployment = test::kValidMinimalAsilInstanceDeployment; + auto* const lola_service_instance_deployment = + std::get_if(&service_instance_deployment.bindingInfo_); + ASSERT_NE(lola_service_instance_deployment, nullptr); + lola_service_instance_deployment->inter_vm_support_ = true; + lola_service_instance_deployment->inter_vm_forwarded_ = true; + + const auto instance_identifier = + make_InstanceIdentifier(service_instance_deployment, test::kValidMinimalTypeDeployment); + InitialiseSkeleton(instance_identifier); + + EXPECT_CALL(*fcntl_mock_, flock(test::kServiceInstanceUsageFileDescriptor, kNonBlockingExclusiveLockOperation)) + .Times(2) + .WillRepeatedly(Return(score::cpp::blank{})); + EXPECT_CALL(*fcntl_mock_, flock(test::kServiceInstanceUsageFileDescriptor, kUnlockOperation)) + .Times(2) + .WillRepeatedly(Return(score::cpp::blank{})); + + EXPECT_CALL(shared_memory_factory_mock_, Remove(test::kControlChannelPathQm)).Times(0); + EXPECT_CALL(shared_memory_factory_mock_, Remove(test::kControlChannelPathAsilB)).Times(0); + EXPECT_CALL(shared_memory_factory_mock_, Remove(test::kDataChannelPath)).Times(0); + + EXPECT_CALL(*unistd_mock_, close(test::kServiceInstanceUsageFileDescriptor)) + .WillOnce(InvokeWithoutArgs( + [&was_usage_marker_file_closed]() mutable noexcept -> score::cpp::expected_blank { + was_usage_marker_file_closed = true; + return {}; + })); + + EXPECT_TRUE(skeleton_->PrepareOffer(events_, fields_, std::move(kEmptyRegisterShmObjectTraceCallback)).has_value()); + + EXPECT_FALSE(was_usage_marker_file_closed); + skeleton_->PrepareStopOffer({}); + EXPECT_TRUE(was_usage_marker_file_closed); +} + using SkeletonDisconnectQmConsumersFixture = SkeletonTestMockedSharedMemoryFixture; TEST_F(SkeletonDisconnectQmConsumersFixture, CallingDisconnectQmConsumersCallsStopOfferServiceOnServiceDiscoveryBinding) { @@ -1323,6 +1409,143 @@ TEST_P(SkeletonRegisterParamaterisedFixture, CallingRegisterWithSameServiceEleme EXPECT_DEATH(test_function(), ".*"); } +TEST_P(SkeletonRegisterParamaterisedFixture, RegisterDiesWhenCalledForGatewayForwardedSkeleton) +{ + const ServiceElementType element_type = GetParam(); + + if (element_type == ServiceElementType::EVENT) + { + events_.emplace(test::kFooEventName, mock_event_binding_); + } + else + { + fields_.emplace(test::kFooEventName, mock_event_binding_); + } + + // Given a gateway-forwarded ASIL-B skeleton deployment (inter_vm_support_ and inter_vm_forwarded_ set) + auto service_instance_deployment = element_type == ServiceElementType::EVENT + ? test::kValidAsilInstanceDeploymentWithEvent + : test::kValidAsilInstanceDeploymentWithField; + auto* const lola_deployment = std::get_if(&service_instance_deployment.bindingInfo_); + ASSERT_NE(lola_deployment, nullptr); + lola_deployment->inter_vm_support_ = true; + lola_deployment->inter_vm_forwarded_ = true; + const auto gateway_instance_identifier = + make_InstanceIdentifier(service_instance_deployment, test::kValidMinimalTypeDeployment); + + // When PrepareOffer is called it opens (not creates) the existing shared memory (use_gateway_forwarded_shm_ = true) + InitialiseSkeleton(gateway_instance_identifier).WithNoConnectedProxy(); + EXPECT_TRUE(skeleton_->PrepareOffer(events_, fields_, std::move(kEmptyRegisterShmObjectTraceCallback)).has_value()); + + // Then calling the typed Register on a gateway-forwarded skeleton must die: + // gateway setups use GenericSkeleton exclusively — typed Register is a programming error in this context. + EXPECT_DEATH(skeleton_->Register(test::kDummyElementFqId, test::kDefaultEventProperties), ""); +} + +TEST_P(SkeletonRegisterParamaterisedFixture, RegisterGenericWillOpenEventDataForGatewayForwardedSkeleton) +{ + const ServiceElementType element_type = GetParam(); + + if (element_type == ServiceElementType::EVENT) + { + events_.emplace(test::kFooEventName, mock_event_binding_); + } + else + { + fields_.emplace(test::kFooEventName, mock_event_binding_); + } + + // Given a gateway-forwarded ASIL-B skeleton deployment (inter_vm_support_ and inter_vm_forwarded_ set) + auto service_instance_deployment = element_type == ServiceElementType::EVENT + ? test::kValidAsilInstanceDeploymentWithEvent + : test::kValidAsilInstanceDeploymentWithField; + auto* const lola_deployment = std::get_if(&service_instance_deployment.bindingInfo_); + ASSERT_NE(lola_deployment, nullptr); + lola_deployment->inter_vm_support_ = true; + lola_deployment->inter_vm_forwarded_ = true; + const auto gateway_instance_identifier = + make_InstanceIdentifier(service_instance_deployment, test::kValidMinimalTypeDeployment); + + // When PrepareOffer is called it opens (not creates) the existing shared memory (use_gateway_forwarded_shm_ = true) + InitialiseSkeleton(gateway_instance_identifier).WithNoConnectedProxy(); + EXPECT_TRUE(skeleton_->PrepareOffer(events_, fields_, std::move(kEmptyRegisterShmObjectTraceCallback)).has_value()); + + // When RegisterGeneric is called on the gateway-forwarded skeleton + const auto result = skeleton_->RegisterGeneric( + test::kDummyElementFqId, test::kDefaultEventProperties, sizeof(std::uint8_t), alignof(std::uint8_t)); + + // Then it returns valid pointers to the opened SHM data and event controls (not newly created) + EXPECT_NE(result.type_erased_event_data_storage_ptr, nullptr); + EXPECT_NE(result.event_control_asil_b, nullptr); +} + +TEST_P(SkeletonRegisterParamaterisedFixture, RegisterGenericWillOpenEventDataForReopenedSkeleton) +{ + const ServiceElementType element_type = GetParam(); + + if (element_type == ServiceElementType::EVENT) + { + events_.emplace(test::kFooEventName, mock_event_binding_); + } + else + { + fields_.emplace(test::kFooEventName, mock_event_binding_); + } + const InstanceIdentifier instance_identifier{element_type == ServiceElementType::EVENT + ? GetValidASILInstanceIdentifierWithEvent() + : GetValidASILInstanceIdentifierWithField()}; + + // Given a Skeleton with an already-connected proxy: flock of usage marker file fails, + // causing PrepareOffer to go through kReuseExistingShm and set was_old_shm_region_reopened_ = true + InitialiseSkeleton(instance_identifier).WithAlreadyConnectedProxy(); + EXPECT_TRUE(skeleton_->PrepareOffer(events_, fields_, std::move(kEmptyRegisterShmObjectTraceCallback)).has_value()); + + // When RegisterGeneric is called on the reopened skeleton + const auto result = skeleton_->RegisterGeneric( + test::kDummyElementFqId, test::kDefaultEventProperties, sizeof(std::uint8_t), alignof(std::uint8_t)); + + // Then it returns valid pointers to the opened (not newly created) SHM data and event controls + EXPECT_NE(result.type_erased_event_data_storage_ptr, nullptr); + EXPECT_NE(result.event_control_asil_b, nullptr); +} + +TEST_P(SkeletonRegisterParamaterisedFixture, RegisterGenericWillRollbackTransactionLogForReopenedSkeleton) +{ + // Given a QM ServiceDataControl which contains a TransactionLogSet with valid transactions + auto proxy_event_data_control_qm_local = GetConsumerEventDataControlLocalFromServiceDataControl( + test::kDummyElementFqId, *existing_service_data_control_qm_); + auto& transaction_log_set = + GetTransactionLogSetFromServiceDataControl(test::kDummyElementFqId, *existing_service_data_control_qm_); + InsertSkeletonTransactionLogWithValidTransactions(proxy_event_data_control_qm_local, transaction_log_set); + EXPECT_TRUE(IsSkeletonTransactionLogRegistered(transaction_log_set)); + + const ServiceElementType element_type = GetParam(); + + if (element_type == ServiceElementType::EVENT) + { + events_.emplace(test::kFooEventName, mock_event_binding_); + } + else + { + fields_.emplace(test::kFooEventName, mock_event_binding_); + } + const InstanceIdentifier instance_identifier{element_type == ServiceElementType::EVENT + ? GetValidInstanceIdentifierWithEvent() + : GetValidInstanceIdentifierWithField()}; + + // Given a Skeleton with an already-connected proxy: flock of usage marker file fails, + // causing PrepareOffer to go through kReuseExistingShm and set was_old_shm_region_reopened_ = true + InitialiseSkeleton(instance_identifier).WithAlreadyConnectedProxy(); + EXPECT_TRUE(skeleton_->PrepareOffer(events_, fields_, std::move(kEmptyRegisterShmObjectTraceCallback)).has_value()); + + // When RegisterGeneric is called on the reopened skeleton + score::cpp::ignore = skeleton_->RegisterGeneric( + test::kDummyElementFqId, test::kDefaultEventProperties, sizeof(std::uint8_t), alignof(std::uint8_t)); + + // Then the QM TransactionLog should be rolled back and unregistered + EXPECT_FALSE(IsSkeletonTransactionLogRegistered(transaction_log_set)); +} + INSTANTIATE_TEST_SUITE_P(SkeletonRegisterParamaterisedFixture, SkeletonRegisterParamaterisedFixture, Values(ServiceElementType::EVENT, ServiceElementType::FIELD));