From ff3610f6eb0e9778e62519d5dbf20a19c72db4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Tue, 30 Jun 2026 08:30:48 +0200 Subject: [PATCH 01/10] initial discovery as c-api draft MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- SilKit/include/silkit/capi/Experimental.h | 170 ++++++++++++++ SilKit/include/silkit/capi/SilKit.h | 1 + SilKit/include/silkit/capi/Types.h | 14 ++ SilKit/source/capi/CMakeLists.txt | 2 + SilKit/source/capi/CapiExperimental.cpp | 169 ++++++++++++++ .../source/capi/Test_CapiServiceDiscovery.cpp | 220 ++++++++++++++++++ SilKit/source/capi/Test_CapiSymbols.cpp | 7 + .../core/internal/ServiceDescriptor.hpp | 8 + 8 files changed, 591 insertions(+) create mode 100644 SilKit/include/silkit/capi/Experimental.h create mode 100644 SilKit/source/capi/CapiExperimental.cpp create mode 100644 SilKit/source/capi/Test_CapiServiceDiscovery.cpp diff --git a/SilKit/include/silkit/capi/Experimental.h b/SilKit/include/silkit/capi/Experimental.h new file mode 100644 index 000000000..b9f3a7d3f --- /dev/null +++ b/SilKit/include/silkit/capi/Experimental.h @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once +#include +#include "silkit/capi/SilKitMacros.h" +#include "silkit/capi/Types.h" +#include "silkit/capi/InterfaceIdentifiers.h" + +#pragma pack(push) +#pragma pack(8) + +SILKIT_BEGIN_DECLS + +// ============================================================================ +// Experimental service discovery +// +// Allows a participant to passively observe the services (bus controllers, +// publishers/subscribers, RPC clients/servers, ...) created by all other +// participants in the simulation. This mirrors the internal service discovery +// used throughout the SIL Kit and requires no configuration of the observed +// participants. +// +// \warning The functions and types declared in this header are not part of the +// stable API and ABI of the SIL Kit. They may be removed or changed at +// any time without prior notice. +// ============================================================================ + +/*! \brief The kind of change reported for a discovered service. */ +typedef uint32_t SilKit_Experimental_ServiceDiscoveryEvent_Type; +/*! \brief An invalid / unknown service discovery event. */ +#define SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid ((SilKit_Experimental_ServiceDiscoveryEvent_Type)0) +/*! \brief A service has been created (or was already present on registration). */ +#define SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated \ + ((SilKit_Experimental_ServiceDiscoveryEvent_Type)1) +/*! \brief A service has been removed. */ +#define SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved \ + ((SilKit_Experimental_ServiceDiscoveryEvent_Type)2) + +/*! \brief The type of a discovered service. Matches SilKit::Core::ServiceType. */ +typedef uint32_t SilKit_Experimental_ServiceType; +#define SilKit_Experimental_ServiceType_Undefined ((SilKit_Experimental_ServiceType)0) +#define SilKit_Experimental_ServiceType_Link ((SilKit_Experimental_ServiceType)1) +#define SilKit_Experimental_ServiceType_Controller ((SilKit_Experimental_ServiceType)2) +#define SilKit_Experimental_ServiceType_SimulatedController ((SilKit_Experimental_ServiceType)3) +#define SilKit_Experimental_ServiceType_InternalController ((SilKit_Experimental_ServiceType)4) + +/*! \brief Handler invoked when a service is created or removed in the simulation. + * + * The \p serviceDescriptor handle and any string returned by the + * SilKit_Experimental_ServiceDescriptor_... accessors are only valid for the + * duration of the handler invocation. Copy the data if it must outlive the call. + * + * \param context The user context pointer passed to \ref SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler. + * \param eventType Whether the service was created or removed. + * \param serviceDescriptor Opaque handle describing the affected service. + */ +typedef void(SilKitFPTR* SilKit_Experimental_ServiceDiscoveryHandler_t)( + void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type eventType, + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor); + +/*! \brief Obtain the experimental service discovery observer of a participant. + * + * The returned object is owned by the participant and must not be destroyed by the caller. + * + * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without + * prior notice. + * + * \param outServiceDiscovery Pointer through which the service discovery observer is returned (out parameter). + * \param participant The participant instance for which the observer is obtained. + */ +SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_Create( + SilKit_Experimental_ServiceDiscovery** outServiceDiscovery, SilKit_Participant* participant); + +typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDiscovery_Create_t)( + SilKit_Experimental_ServiceDiscovery** outServiceDiscovery, SilKit_Participant* participant); + +/*! \brief Register a handler that is called for every service in the simulation. + * + * Upon registration the handler is immediately invoked once for every service that is already known, each reported as + * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated. It is subsequently invoked for every service + * created or removed. + * + * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without + * prior notice. + * + * \param serviceDiscovery The observer obtained via \ref SilKit_Experimental_ServiceDiscovery_Create. + * \param context The user context pointer made available to the handler. + * \param handler The handler to be called on service creation and removal. + */ +SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler( + SilKit_Experimental_ServiceDiscovery* serviceDiscovery, void* context, + SilKit_Experimental_ServiceDiscoveryHandler_t handler); + +typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler_t)( + SilKit_Experimental_ServiceDiscovery* serviceDiscovery, void* context, + SilKit_Experimental_ServiceDiscoveryHandler_t handler); + +/*! \brief Return the name of the participant providing the service. + * + * The returned string is owned by the service descriptor and is only valid for the duration of the handler invocation. + * + * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without + * prior notice. + */ +SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetParticipantName( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outParticipantName); + +typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDescriptor_GetParticipantName_t)( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outParticipantName); + +/*! \brief Return the name of the service. + * + * The returned string is owned by the service descriptor and is only valid for the duration of the handler invocation. + * + * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without + * prior notice. + */ +SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetServiceName( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outServiceName); + +typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDescriptor_GetServiceName_t)( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outServiceName); + +/*! \brief Return the network name the service is associated with. + * + * The returned string is owned by the service descriptor and is only valid for the duration of the handler invocation. + * + * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without + * prior notice. + */ +SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetNetworkName( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outNetworkName); + +typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDescriptor_GetNetworkName_t)( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outNetworkName); + +/*! \brief Return the type of the service. + * + * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without + * prior notice. + */ +SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetServiceType( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, SilKit_Experimental_ServiceType* outServiceType); + +typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDescriptor_GetServiceType_t)( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, SilKit_Experimental_ServiceType* outServiceType); + +/*! \brief Look up a value in the supplemental data of the service by key (e.g. "topic", "controllerType"). + * + * If the key is present, \p outHasValue is set to \ref SilKit_True and \p outValue points to the value. If the key is + * absent, \p outHasValue is set to \ref SilKit_False, \p outValue is set to NULL, and \ref SilKit_ReturnCode_SUCCESS is + * still returned. The returned string is owned by the service descriptor and is only valid for the duration of the + * handler invocation. + * + * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without + * prior notice. + */ +SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char* key, const char** outValue, + SilKit_Bool* outHasValue); + +typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem_t)( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char* key, const char** outValue, + SilKit_Bool* outHasValue); + +SILKIT_END_DECLS + +#pragma pack(pop) diff --git a/SilKit/include/silkit/capi/SilKit.h b/SilKit/include/silkit/capi/SilKit.h index cfafcfb34..00d82d442 100644 --- a/SilKit/include/silkit/capi/SilKit.h +++ b/SilKit/include/silkit/capi/SilKit.h @@ -20,6 +20,7 @@ #include "silkit/capi/Version.h" #include "silkit/capi/NetworkSimulator.h" #include "silkit/capi/EventProducer.h" +#include "silkit/capi/Experimental.h" SILKIT_BEGIN_DECLS diff --git a/SilKit/include/silkit/capi/Types.h b/SilKit/include/silkit/capi/Types.h index a3f50f78a..8c0ad50cb 100644 --- a/SilKit/include/silkit/capi/Types.h +++ b/SilKit/include/silkit/capi/Types.h @@ -24,6 +24,20 @@ typedef struct SilKit_Vendor_Vector_SilKitRegistry SilKit_Vendor_Vector_SilKitRe */ typedef struct SilKit_Experimental_SystemController SilKit_Experimental_SystemController; +/*! \brief Opaque type. Used in functions prefixed with SilKit_Experimental_ServiceDiscovery_.... + * + * \warning This type is not part of the stable API of the SIL Kit. It may be removed at any time without prior notice. + */ +typedef struct SilKit_Experimental_ServiceDiscovery SilKit_Experimental_ServiceDiscovery; + +/*! \brief Opaque type describing a single discovered service. Passed to a + * \ref SilKit_Experimental_ServiceDiscoveryHandler_t and queried with the + * SilKit_Experimental_ServiceDescriptor_... accessor functions. + * + * \warning This type is not part of the stable API of the SIL Kit. It may be removed at any time without prior notice. + */ +typedef struct SilKit_Experimental_ServiceDescriptor SilKit_Experimental_ServiceDescriptor; + typedef int32_t SilKit_ReturnCode; #define SilKit_ReturnCode_SUCCESS ((SilKit_ReturnCode)0) diff --git a/SilKit/source/capi/CMakeLists.txt b/SilKit/source/capi/CMakeLists.txt index 59e74758d..046a14513 100644 --- a/SilKit/source/capi/CMakeLists.txt +++ b/SilKit/source/capi/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(O_SilKit_Capi OBJECT CapiDataPubSub.cpp CapiEthernet.cpp CapiExceptions.hpp + CapiExperimental.cpp CapiFlexray.cpp CapiImpl.hpp CapiLin.cpp @@ -30,6 +31,7 @@ add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiCan.cpp LIBS O_Si add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiEthernet.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiFlexray.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiData.cpp LIBS S_SilKitImpl I_SilKit) +add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiServiceDiscovery.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiRpc.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiLogger.cpp LIBS S_SilKitImpl) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiVersion.cpp LIBS S_SilKitImpl) diff --git a/SilKit/source/capi/CapiExperimental.cpp b/SilKit/source/capi/CapiExperimental.cpp new file mode 100644 index 000000000..3993fcf56 --- /dev/null +++ b/SilKit/source/capi/CapiExperimental.cpp @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "silkit/capi/SilKit.h" +#include "silkit/SilKit.hpp" +#include "silkit/participant/exception.hpp" + +#include "capi/CapiImpl.hpp" + +#include "core/internal/IParticipantInternal.hpp" +#include "core/service/IServiceDiscovery.hpp" +#include "core/internal/ServiceDescriptor.hpp" + +namespace { + +auto GetServiceDiscovery(SilKit_Participant* participant) -> SilKit::Core::Discovery::IServiceDiscovery* +{ + auto* cppParticipant = reinterpret_cast(participant); + auto* participantInternal = dynamic_cast(cppParticipant); + if (participantInternal == nullptr) + { + throw SilKit::SilKitError{"participant is not a valid SilKit::IParticipant*"}; + } + return participantInternal->GetServiceDiscovery(); +} + +auto ToC(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type) + -> SilKit_Experimental_ServiceDiscoveryEvent_Type +{ + using Type = SilKit::Core::Discovery::ServiceDiscoveryEvent::Type; + switch (type) + { + case Type::ServiceCreated: + return SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; + case Type::ServiceRemoved: + return SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; + default: + return SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid; + } +} + +auto FromC(const SilKit_Experimental_ServiceDescriptor* serviceDescriptor) -> const SilKit::Core::ServiceDescriptor* +{ + return reinterpret_cast(serviceDescriptor); +} + +} // namespace + + +SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_Create( + SilKit_Experimental_ServiceDiscovery** outServiceDiscovery, SilKit_Participant* participant) +try +{ + ASSERT_VALID_OUT_PARAMETER(outServiceDiscovery); + ASSERT_VALID_POINTER_PARAMETER(participant); + + auto* serviceDiscovery = GetServiceDiscovery(participant); + *outServiceDiscovery = reinterpret_cast(serviceDiscovery); + + return SilKit_ReturnCode_SUCCESS; +} +CAPI_CATCH_EXCEPTIONS + + +SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler( + SilKit_Experimental_ServiceDiscovery* serviceDiscovery, void* context, + SilKit_Experimental_ServiceDiscoveryHandler_t handler) +try +{ + ASSERT_VALID_POINTER_PARAMETER(serviceDiscovery); + ASSERT_VALID_HANDLER_PARAMETER(handler); + + auto* cppServiceDiscovery = reinterpret_cast(serviceDiscovery); + + cppServiceDiscovery->RegisterServiceDiscoveryHandler( + [handler, context](SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& serviceDescriptor) { + handler(context, ToC(type), + reinterpret_cast(&serviceDescriptor)); + }); + + return SilKit_ReturnCode_SUCCESS; +} +CAPI_CATCH_EXCEPTIONS + + +SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetParticipantName( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outParticipantName) +try +{ + ASSERT_VALID_POINTER_PARAMETER(serviceDescriptor); + ASSERT_VALID_OUT_PARAMETER(outParticipantName); + + *outParticipantName = FromC(serviceDescriptor)->GetParticipantName().c_str(); + + return SilKit_ReturnCode_SUCCESS; +} +CAPI_CATCH_EXCEPTIONS + + +SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetServiceName( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outServiceName) +try +{ + ASSERT_VALID_POINTER_PARAMETER(serviceDescriptor); + ASSERT_VALID_OUT_PARAMETER(outServiceName); + + *outServiceName = FromC(serviceDescriptor)->GetServiceName().c_str(); + + return SilKit_ReturnCode_SUCCESS; +} +CAPI_CATCH_EXCEPTIONS + + +SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetNetworkName( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outNetworkName) +try +{ + ASSERT_VALID_POINTER_PARAMETER(serviceDescriptor); + ASSERT_VALID_OUT_PARAMETER(outNetworkName); + + *outNetworkName = FromC(serviceDescriptor)->GetNetworkName().c_str(); + + return SilKit_ReturnCode_SUCCESS; +} +CAPI_CATCH_EXCEPTIONS + + +SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetServiceType( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, SilKit_Experimental_ServiceType* outServiceType) +try +{ + ASSERT_VALID_POINTER_PARAMETER(serviceDescriptor); + ASSERT_VALID_OUT_PARAMETER(outServiceType); + + *outServiceType = static_cast(FromC(serviceDescriptor)->GetServiceType()); + + return SilKit_ReturnCode_SUCCESS; +} +CAPI_CATCH_EXCEPTIONS + + +SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem( + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char* key, const char** outValue, + SilKit_Bool* outHasValue) +try +{ + ASSERT_VALID_POINTER_PARAMETER(serviceDescriptor); + ASSERT_VALID_POINTER_PARAMETER(key); + ASSERT_VALID_OUT_PARAMETER(outValue); + ASSERT_VALID_OUT_PARAMETER(outHasValue); + + const auto& supplementalData = FromC(serviceDescriptor)->GetSupplementalDataRef(); + const auto it = supplementalData.find(key); + if (it == supplementalData.end()) + { + *outValue = nullptr; + *outHasValue = SilKit_False; + } + else + { + *outValue = it->second.c_str(); + *outHasValue = SilKit_True; + } + + return SilKit_ReturnCode_SUCCESS; +} +CAPI_CATCH_EXCEPTIONS diff --git a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp new file mode 100644 index 000000000..1d498ef46 --- /dev/null +++ b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "silkit/capi/SilKit.h" + +#include "core/mock/participant/MockParticipant.hpp" +#include "core/internal/ServiceDescriptor.hpp" + +namespace { + +using SilKit::Core::Tests::DummyParticipant; +using ServiceDiscoveryEvent = SilKit::Core::Discovery::ServiceDiscoveryEvent; + +// Captures, inside the handler invocation, everything the C accessors return. Copying into owned strings here also +// verifies that the descriptor handle and the returned pointers are valid for the duration of the callback. +struct CallbackData +{ + int callCount{0}; + SilKit_Experimental_ServiceDiscoveryEvent_Type lastType{ + SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid}; + std::string participantName; + std::string serviceName; + std::string networkName; + SilKit_Experimental_ServiceType serviceType{SilKit_Experimental_ServiceType_Undefined}; + bool hasTopic{false}; + std::string topic; +}; + +void SilKitCALL TestDiscoveryHandler(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor) +{ + auto* data = static_cast(context); + data->callCount += 1; + data->lastType = type; + + const char* str = nullptr; + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetParticipantName(serviceDescriptor, &str), + SilKit_ReturnCode_SUCCESS); + data->participantName = str; + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceName(serviceDescriptor, &str), + SilKit_ReturnCode_SUCCESS); + data->serviceName = str; + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetNetworkName(serviceDescriptor, &str), + SilKit_ReturnCode_SUCCESS); + data->networkName = str; + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceType(serviceDescriptor, &data->serviceType), + SilKit_ReturnCode_SUCCESS); + + SilKit_Bool hasValue = SilKit_False; + const char* value = nullptr; + EXPECT_EQ( + SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(serviceDescriptor, "topic", &value, &hasValue), + SilKit_ReturnCode_SUCCESS); + data->hasTopic = (hasValue == SilKit_True); + if (data->hasTopic) + { + data->topic = value; + } +} + +void SilKitCALL NoopDiscoveryHandler(void* /*context*/, SilKit_Experimental_ServiceDiscoveryEvent_Type /*type*/, + const SilKit_Experimental_ServiceDescriptor* /*serviceDescriptor*/) +{ +} + +class Test_CapiServiceDiscovery : public testing::Test +{ +public: + DummyParticipant mockParticipant; + + // Build a populated descriptor for the accessor tests. + static SilKit::Core::ServiceDescriptor MakeDescriptor() + { + SilKit::Core::ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ParticipantA"); + descriptor.SetServiceName("MyPublisher"); + descriptor.SetNetworkName("NetworkX"); + descriptor.SetServiceType(SilKit::Core::ServiceType::Controller); + descriptor.SetSupplementalDataItem("topic", "TopicA"); + return descriptor; + } + + static const SilKit_Experimental_ServiceDescriptor* AsC(const SilKit::Core::ServiceDescriptor& descriptor) + { + return reinterpret_cast(&descriptor); + } +}; + +TEST_F(Test_CapiServiceDiscovery, create_returns_service_discovery) +{ + SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; + const auto returnCode = + SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant); + EXPECT_EQ(returnCode, SilKit_ReturnCode_SUCCESS); + EXPECT_NE(serviceDiscovery, nullptr); +} + +TEST_F(Test_CapiServiceDiscovery, create_bad_parameters) +{ + SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; + + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(nullptr, (SilKit_Participant*)&mockParticipant), + SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, nullptr), + SilKit_ReturnCode_BADPARAMETER); +} + +TEST_F(Test_CapiServiceDiscovery, set_handler_bad_parameters) +{ + SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), + SilKit_ReturnCode_SUCCESS); + + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(nullptr, nullptr, &NoopDiscoveryHandler), + SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, nullptr, nullptr), + SilKit_ReturnCode_BADPARAMETER); +} + +TEST_F(Test_CapiServiceDiscovery, set_handler_registers_and_forwards_events) +{ + SilKit::Core::Discovery::ServiceDiscoveryHandler capturedHandler; + EXPECT_CALL(mockParticipant.mockServiceDiscovery, RegisterServiceDiscoveryHandler(testing::_)) + .WillOnce(testing::SaveArg<0>(&capturedHandler)); + + SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), + SilKit_ReturnCode_SUCCESS); + + CallbackData data; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, &data, + &TestDiscoveryHandler), + SilKit_ReturnCode_SUCCESS); + ASSERT_TRUE(static_cast(capturedHandler)); + + const auto descriptor = MakeDescriptor(); + + capturedHandler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.participantName, "ParticipantA"); + EXPECT_EQ(data.serviceName, "MyPublisher"); + EXPECT_EQ(data.networkName, "NetworkX"); + EXPECT_EQ(data.serviceType, SilKit_Experimental_ServiceType_Controller); + EXPECT_TRUE(data.hasTopic); + EXPECT_EQ(data.topic, "TopicA"); + + capturedHandler(ServiceDiscoveryEvent::Type::ServiceRemoved, descriptor); + EXPECT_EQ(data.callCount, 2); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); +} + +TEST_F(Test_CapiServiceDiscovery, descriptor_accessors) +{ + const auto descriptor = MakeDescriptor(); + const auto* cDescriptor = AsC(descriptor); + + const char* str = nullptr; + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetParticipantName(cDescriptor, &str), SilKit_ReturnCode_SUCCESS); + EXPECT_STREQ(str, "ParticipantA"); + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceName(cDescriptor, &str), SilKit_ReturnCode_SUCCESS); + EXPECT_STREQ(str, "MyPublisher"); + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetNetworkName(cDescriptor, &str), SilKit_ReturnCode_SUCCESS); + EXPECT_STREQ(str, "NetworkX"); + + SilKit_Experimental_ServiceType serviceType = SilKit_Experimental_ServiceType_Undefined; + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceType(cDescriptor, &serviceType), + SilKit_ReturnCode_SUCCESS); + EXPECT_EQ(serviceType, SilKit_Experimental_ServiceType_Controller); + + SilKit_Bool hasValue = SilKit_False; + const char* value = nullptr; + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(cDescriptor, "topic", &value, &hasValue), + SilKit_ReturnCode_SUCCESS); + EXPECT_EQ(hasValue, SilKit_True); + EXPECT_STREQ(value, "TopicA"); + + // Absent key is not an error: SUCCESS with hasValue == SilKit_False and value == nullptr. + EXPECT_EQ( + SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(cDescriptor, "missing", &value, &hasValue), + SilKit_ReturnCode_SUCCESS); + EXPECT_EQ(hasValue, SilKit_False); + EXPECT_EQ(value, nullptr); +} + +TEST_F(Test_CapiServiceDiscovery, descriptor_accessors_bad_parameters) +{ + const auto descriptor = MakeDescriptor(); + const auto* cDescriptor = AsC(descriptor); + + const char* str = nullptr; + SilKit_Experimental_ServiceType serviceType = SilKit_Experimental_ServiceType_Undefined; + SilKit_Bool hasValue = SilKit_False; + const char* value = nullptr; + + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetParticipantName(nullptr, &str), SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetParticipantName(cDescriptor, nullptr), + SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceName(nullptr, &str), SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetNetworkName(nullptr, &str), SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceType(nullptr, &serviceType), + SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceType(cDescriptor, nullptr), + SilKit_ReturnCode_BADPARAMETER); + + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(nullptr, "topic", &value, &hasValue), + SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(cDescriptor, nullptr, &value, &hasValue), + SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(cDescriptor, "topic", nullptr, &hasValue), + SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(cDescriptor, "topic", &value, nullptr), + SilKit_ReturnCode_BADPARAMETER); +} + +} // namespace diff --git a/SilKit/source/capi/Test_CapiSymbols.cpp b/SilKit/source/capi/Test_CapiSymbols.cpp index 8f0612407..a9cc3c163 100644 --- a/SilKit/source/capi/Test_CapiSymbols.cpp +++ b/SilKit/source/capi/Test_CapiSymbols.cpp @@ -129,6 +129,13 @@ TEST(Test_CapiSymbols, DISABLED_link_all_public_symbols) (void)SilKit_Experimental_NetworkSimulator_Start(nullptr); (void)SilKit_Experimental_NetworkSimulator_SimulateNetwork(nullptr, "", SilKit_NetworkType_Undefined, nullptr, nullptr); + (void)SilKit_Experimental_ServiceDiscovery_Create(nullptr, nullptr); + (void)SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(nullptr, nullptr, nullptr); + (void)SilKit_Experimental_ServiceDescriptor_GetParticipantName(nullptr, nullptr); + (void)SilKit_Experimental_ServiceDescriptor_GetServiceName(nullptr, nullptr); + (void)SilKit_Experimental_ServiceDescriptor_GetNetworkName(nullptr, nullptr); + (void)SilKit_Experimental_ServiceDescriptor_GetServiceType(nullptr, nullptr); + (void)SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(nullptr, nullptr, nullptr, nullptr); } } // namespace diff --git a/SilKit/source/core/internal/ServiceDescriptor.hpp b/SilKit/source/core/internal/ServiceDescriptor.hpp index ececc8ebd..65b4d4b74 100644 --- a/SilKit/source/core/internal/ServiceDescriptor.hpp +++ b/SilKit/source/core/internal/ServiceDescriptor.hpp @@ -81,6 +81,9 @@ class ServiceDescriptor inline void SetServiceId(SilKit::Core::EndpointId val); inline auto GetSupplementalData() const -> SupplementalData; + //! \brief Direct access to the supplemental data without copying. The reference is valid for the lifetime of this + //! ServiceDescriptor. + inline auto GetSupplementalDataRef() const -> const SupplementalData&; inline void SetSupplementalData(SupplementalData val); inline bool GetSupplementalDataItem(const std::string& key, std::string& value) const; @@ -226,6 +229,11 @@ auto ServiceDescriptor::GetSupplementalData() const -> SupplementalData return _supplementalData; } +auto ServiceDescriptor::GetSupplementalDataRef() const -> const SupplementalData& +{ + return _supplementalData; +} + void ServiceDescriptor::SetSupplementalData(SupplementalData val) { _supplementalData = std::move(val); From ad702effe68e0d0c07f430761c8a6280a094abb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Tue, 30 Jun 2026 08:59:04 +0200 Subject: [PATCH 02/10] capi: refine service discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- SilKit/include/silkit/capi/Experimental.h | 142 +++------ .../silkit/capi/InterfaceIdentifiers.h | 4 + SilKit/include/silkit/capi/Types.h | 8 - SilKit/source/capi/CapiExperimental.cpp | 266 ++++++++++------ SilKit/source/capi/Test_CapiInterfaces.cpp | 1 + .../source/capi/Test_CapiServiceDiscovery.cpp | 300 +++++++++++------- SilKit/source/capi/Test_CapiSymbols.cpp | 5 - 7 files changed, 406 insertions(+), 320 deletions(-) diff --git a/SilKit/include/silkit/capi/Experimental.h b/SilKit/include/silkit/capi/Experimental.h index b9f3a7d3f..48b441e12 100644 --- a/SilKit/include/silkit/capi/Experimental.h +++ b/SilKit/include/silkit/capi/Experimental.h @@ -16,11 +16,11 @@ SILKIT_BEGIN_DECLS // ============================================================================ // Experimental service discovery // -// Allows a participant to passively observe the services (bus controllers, -// publishers/subscribers, RPC clients/servers, ...) created by all other -// participants in the simulation. This mirrors the internal service discovery -// used throughout the SIL Kit and requires no configuration of the observed -// participants. +// Allows a participant to passively observe the user-facing services (bus +// controllers, publishers/subscribers, RPC clients/servers, network links) +// created by all other participants in the simulation. This builds on the +// internal service discovery used throughout the SIL Kit and requires no +// configuration of the observed participants. // // \warning The functions and types declared in this header are not part of the // stable API and ABI of the SIL Kit. They may be removed or changed at @@ -38,23 +38,53 @@ typedef uint32_t SilKit_Experimental_ServiceDiscoveryEvent_Type; #define SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved \ ((SilKit_Experimental_ServiceDiscoveryEvent_Type)2) -/*! \brief The type of a discovered service. Matches SilKit::Core::ServiceType. */ -typedef uint32_t SilKit_Experimental_ServiceType; -#define SilKit_Experimental_ServiceType_Undefined ((SilKit_Experimental_ServiceType)0) -#define SilKit_Experimental_ServiceType_Link ((SilKit_Experimental_ServiceType)1) -#define SilKit_Experimental_ServiceType_Controller ((SilKit_Experimental_ServiceType)2) -#define SilKit_Experimental_ServiceType_SimulatedController ((SilKit_Experimental_ServiceType)3) -#define SilKit_Experimental_ServiceType_InternalController ((SilKit_Experimental_ServiceType)4) - -/*! \brief Handler invoked when a service is created or removed in the simulation. - * - * The \p serviceDescriptor handle and any string returned by the - * SilKit_Experimental_ServiceDescriptor_... accessors are only valid for the - * duration of the handler invocation. Copy the data if it must outlive the call. +/*! \brief The kind of a discovered service. Only user-facing services are reported. */ +typedef uint32_t SilKit_Experimental_ServiceKind; +#define SilKit_Experimental_ServiceKind_Undefined ((SilKit_Experimental_ServiceKind)0) +#define SilKit_Experimental_ServiceKind_CanController ((SilKit_Experimental_ServiceKind)1) +#define SilKit_Experimental_ServiceKind_EthernetController ((SilKit_Experimental_ServiceKind)2) +#define SilKit_Experimental_ServiceKind_FlexrayController ((SilKit_Experimental_ServiceKind)3) +#define SilKit_Experimental_ServiceKind_LinController ((SilKit_Experimental_ServiceKind)4) +#define SilKit_Experimental_ServiceKind_DataPublisher ((SilKit_Experimental_ServiceKind)5) +#define SilKit_Experimental_ServiceKind_DataSubscriber ((SilKit_Experimental_ServiceKind)6) +#define SilKit_Experimental_ServiceKind_RpcClient ((SilKit_Experimental_ServiceKind)7) +#define SilKit_Experimental_ServiceKind_RpcServer ((SilKit_Experimental_ServiceKind)8) +#define SilKit_Experimental_ServiceKind_NetworkLink ((SilKit_Experimental_ServiceKind)9) + +/*! \brief Describes a single discovered service, passed by value to a service discovery handler. + * + * \warning All pointer members (the strings and the label list) are borrowed and only valid for the duration of the + * handler invocation. Copy any data that must outlive the call. + */ +typedef struct +{ + SilKit_StructHeader structHeader; + //! Name of the participant providing the service. + const char* participantName; + //! Name of the service (the controller / publisher / subscriber / client / server name). + const char* serviceName; + //! The kind of service. + SilKit_Experimental_ServiceKind serviceKind; + //! Raw network/link identifier. The user-facing network name for bus controllers (e.g. "CAN1"); a generated id or + //! "default" for pub/sub and RPC. + const char* networkName; + //! Convenience join key for visualization: the network name for bus controllers and links, the topic for + //! pub/sub, and the function name for RPC. + const char* networkOrTopic; + //! Media type for pub/sub and RPC services; empty string when not applicable. + const char* mediaType; + //! Decoded matching labels for pub/sub and RPC services; empty for bus controllers and links. + SilKit_LabelList labelList; +} SilKit_Experimental_ServiceDescriptor; + +/*! \brief Handler invoked when a user-facing service is created or removed in the simulation. + * + * The \p serviceDescriptor and all of its pointer members are only valid for the duration of the handler invocation. + * Copy the data if it must outlive the call. * * \param context The user context pointer passed to \ref SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler. * \param eventType Whether the service was created or removed. - * \param serviceDescriptor Opaque handle describing the affected service. + * \param serviceDescriptor The affected service. */ typedef void(SilKitFPTR* SilKit_Experimental_ServiceDiscoveryHandler_t)( void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type eventType, @@ -76,11 +106,11 @@ SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_Crea typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDiscovery_Create_t)( SilKit_Experimental_ServiceDiscovery** outServiceDiscovery, SilKit_Participant* participant); -/*! \brief Register a handler that is called for every service in the simulation. +/*! \brief Register a handler that is called for every user-facing service in the simulation. * * Upon registration the handler is immediately invoked once for every service that is already known, each reported as - * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated. It is subsequently invoked for every service - * created or removed. + * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated. It is subsequently invoked for every user-facing + * service created or removed. Internal/infrastructure services are not reported. * * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without * prior notice. @@ -97,74 +127,6 @@ typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDiscovery_SetSe SilKit_Experimental_ServiceDiscovery* serviceDiscovery, void* context, SilKit_Experimental_ServiceDiscoveryHandler_t handler); -/*! \brief Return the name of the participant providing the service. - * - * The returned string is owned by the service descriptor and is only valid for the duration of the handler invocation. - * - * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without - * prior notice. - */ -SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetParticipantName( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outParticipantName); - -typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDescriptor_GetParticipantName_t)( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outParticipantName); - -/*! \brief Return the name of the service. - * - * The returned string is owned by the service descriptor and is only valid for the duration of the handler invocation. - * - * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without - * prior notice. - */ -SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetServiceName( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outServiceName); - -typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDescriptor_GetServiceName_t)( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outServiceName); - -/*! \brief Return the network name the service is associated with. - * - * The returned string is owned by the service descriptor and is only valid for the duration of the handler invocation. - * - * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without - * prior notice. - */ -SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetNetworkName( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outNetworkName); - -typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDescriptor_GetNetworkName_t)( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outNetworkName); - -/*! \brief Return the type of the service. - * - * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without - * prior notice. - */ -SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetServiceType( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, SilKit_Experimental_ServiceType* outServiceType); - -typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDescriptor_GetServiceType_t)( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, SilKit_Experimental_ServiceType* outServiceType); - -/*! \brief Look up a value in the supplemental data of the service by key (e.g. "topic", "controllerType"). - * - * If the key is present, \p outHasValue is set to \ref SilKit_True and \p outValue points to the value. If the key is - * absent, \p outHasValue is set to \ref SilKit_False, \p outValue is set to NULL, and \ref SilKit_ReturnCode_SUCCESS is - * still returned. The returned string is owned by the service descriptor and is only valid for the duration of the - * handler invocation. - * - * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without - * prior notice. - */ -SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char* key, const char** outValue, - SilKit_Bool* outHasValue); - -typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem_t)( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char* key, const char** outValue, - SilKit_Bool* outHasValue); - SILKIT_END_DECLS #pragma pack(pop) diff --git a/SilKit/include/silkit/capi/InterfaceIdentifiers.h b/SilKit/include/silkit/capi/InterfaceIdentifiers.h index 7490bd666..ed62243fe 100644 --- a/SilKit/include/silkit/capi/InterfaceIdentifiers.h +++ b/SilKit/include/silkit/capi/InterfaceIdentifiers.h @@ -240,12 +240,14 @@ typedef struct #define SilKit_LifecycleConfiguration_DATATYPE_ID 2 #define SilKit_WorkflowConfiguration_DATATYPE_ID 3 #define SilKit_ParticipantConnectionInformation_DATATYPE_ID 4 +#define SilKit_Experimental_ServiceDescriptor_DATATYPE_ID 5 // Participant data type Versions #define SilKit_ParticipantStatus_VERSION 1 #define SilKit_LifecycleConfiguration_VERSION 1 #define SilKit_WorkflowConfiguration_VERSION 3 #define SilKit_ParticipantConnectionInformation_VERSION 1 +#define SilKit_Experimental_ServiceDescriptor_VERSION 1 // Participant public API IDs #define SilKit_ParticipantStatus_STRUCT_VERSION SK_ID_MAKE(Participant, SilKit_ParticipantStatus) @@ -253,6 +255,8 @@ typedef struct #define SilKit_WorkflowConfiguration_STRUCT_VERSION SK_ID_MAKE(Participant, SilKit_WorkflowConfiguration) #define SilKit_ParticipantConnectionInformation_STRUCT_VERSION \ SK_ID_MAKE(Participant, SilKit_ParticipantConnectionInformation) +#define SilKit_Experimental_ServiceDescriptor_STRUCT_VERSION \ + SK_ID_MAKE(Participant, SilKit_Experimental_ServiceDescriptor) // NetworkSimulator diff --git a/SilKit/include/silkit/capi/Types.h b/SilKit/include/silkit/capi/Types.h index 8c0ad50cb..9afc391e0 100644 --- a/SilKit/include/silkit/capi/Types.h +++ b/SilKit/include/silkit/capi/Types.h @@ -30,14 +30,6 @@ typedef struct SilKit_Experimental_SystemController SilKit_Experimental_SystemCo */ typedef struct SilKit_Experimental_ServiceDiscovery SilKit_Experimental_ServiceDiscovery; -/*! \brief Opaque type describing a single discovered service. Passed to a - * \ref SilKit_Experimental_ServiceDiscoveryHandler_t and queried with the - * SilKit_Experimental_ServiceDescriptor_... accessor functions. - * - * \warning This type is not part of the stable API of the SIL Kit. It may be removed at any time without prior notice. - */ -typedef struct SilKit_Experimental_ServiceDescriptor SilKit_Experimental_ServiceDescriptor; - typedef int32_t SilKit_ReturnCode; #define SilKit_ReturnCode_SUCCESS ((SilKit_ReturnCode)0) diff --git a/SilKit/source/capi/CapiExperimental.cpp b/SilKit/source/capi/CapiExperimental.cpp index 3993fcf56..4b5fcae3d 100644 --- a/SilKit/source/capi/CapiExperimental.cpp +++ b/SilKit/source/capi/CapiExperimental.cpp @@ -5,16 +5,25 @@ #include "silkit/capi/SilKit.h" #include "silkit/SilKit.hpp" #include "silkit/participant/exception.hpp" +#include "silkit/services/datatypes.hpp" #include "capi/CapiImpl.hpp" #include "core/internal/IParticipantInternal.hpp" #include "core/service/IServiceDiscovery.hpp" #include "core/internal/ServiceDescriptor.hpp" +#include "core/internal/ServiceConfigKeys.hpp" + +#include "config/YamlParser.hpp" + +#include +#include namespace { -auto GetServiceDiscovery(SilKit_Participant* participant) -> SilKit::Core::Discovery::IServiceDiscovery* +namespace Discovery = SilKit::Core::Discovery; + +auto GetServiceDiscovery(SilKit_Participant* participant) -> Discovery::IServiceDiscovery* { auto* cppParticipant = reinterpret_cast(participant); auto* participantInternal = dynamic_cast(cppParticipant); @@ -25,10 +34,9 @@ auto GetServiceDiscovery(SilKit_Participant* participant) -> SilKit::Core::Disco return participantInternal->GetServiceDiscovery(); } -auto ToC(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type) - -> SilKit_Experimental_ServiceDiscoveryEvent_Type +auto ToC(Discovery::ServiceDiscoveryEvent::Type type) -> SilKit_Experimental_ServiceDiscoveryEvent_Type { - using Type = SilKit::Core::Discovery::ServiceDiscoveryEvent::Type; + using Type = Discovery::ServiceDiscoveryEvent::Type; switch (type) { case Type::ServiceCreated: @@ -40,9 +48,151 @@ auto ToC(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type) } } -auto FromC(const SilKit_Experimental_ServiceDescriptor* serviceDescriptor) -> const SilKit::Core::ServiceDescriptor* +// Owns the backing storage for the label list so that the borrowed c-string pointers in the +// SilKit_Experimental_ServiceDescriptor remain valid for the duration of the handler invocation. +struct LabelStorage { - return reinterpret_cast(serviceDescriptor); + std::vector labels; + std::vector cLabels; +}; + +// Maps the internal ServiceDescriptor to the public struct. Returns false if the service is not user-facing +// (infrastructure / internal services), in which case the handler must not be invoked. +auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, + SilKit_Experimental_ServiceDescriptor& out, LabelStorage& storage) -> bool +{ + const auto& supplementalData = serviceDescriptor.GetSupplementalDataRef(); + + const auto findValue = [&supplementalData](const std::string& key) -> const char* { + const auto it = supplementalData.find(key); + return it == supplementalData.end() ? nullptr : it->second.c_str(); + }; + + SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, out); + out.participantName = serviceDescriptor.GetParticipantName().c_str(); + out.serviceName = serviceDescriptor.GetServiceName().c_str(); + out.networkName = serviceDescriptor.GetNetworkName().c_str(); + out.networkOrTopic = serviceDescriptor.GetNetworkName().c_str(); + out.mediaType = ""; + + // Network links carry no controller type and are reported as-is. + if (serviceDescriptor.GetServiceType() == SilKit::Core::ServiceType::Link) + { + out.serviceKind = SilKit_Experimental_ServiceKind_NetworkLink; + return true; + } + + std::string controllerType; + if (!serviceDescriptor.GetSupplementalDataItem(Discovery::controllerType, controllerType)) + { + return false; + } + + // Decode the YAML-encoded matching labels into the public label list. A malformed value must not propagate + // an exception: the service is still reported, just without labels. + const auto decodeLabels = [&](const std::string& labelsKey) { + const char* labelsStr = findValue(labelsKey); + if (labelsStr == nullptr || *labelsStr == '\0') + { + return; + } + try + { + storage.labels = SilKit::Config::Deserialize>(labelsStr); + } + catch (...) + { + return; + } + storage.cLabels.reserve(storage.labels.size()); + for (const auto& label : storage.labels) + { + SilKit_Label cLabel; + cLabel.key = label.key.c_str(); + cLabel.value = label.value.c_str(); + cLabel.kind = static_cast(label.kind); + storage.cLabels.push_back(cLabel); + } + out.labelList.numLabels = storage.cLabels.size(); + out.labelList.labels = storage.cLabels.data(); + }; + + if (controllerType == Discovery::controllerTypeCan) + { + out.serviceKind = SilKit_Experimental_ServiceKind_CanController; + } + else if (controllerType == Discovery::controllerTypeEthernet) + { + out.serviceKind = SilKit_Experimental_ServiceKind_EthernetController; + } + else if (controllerType == Discovery::controllerTypeFlexray) + { + out.serviceKind = SilKit_Experimental_ServiceKind_FlexrayController; + } + else if (controllerType == Discovery::controllerTypeLin) + { + out.serviceKind = SilKit_Experimental_ServiceKind_LinController; + } + else if (controllerType == Discovery::controllerTypeDataPublisher) + { + out.serviceKind = SilKit_Experimental_ServiceKind_DataPublisher; + if (const char* topic = findValue(Discovery::supplKeyDataPublisherTopic)) + { + out.networkOrTopic = topic; + } + if (const char* mediaType = findValue(Discovery::supplKeyDataPublisherMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyDataPublisherPubLabels); + } + else if (controllerType == Discovery::controllerTypeDataSubscriber) + { + out.serviceKind = SilKit_Experimental_ServiceKind_DataSubscriber; + if (const char* topic = findValue(Discovery::supplKeyDataSubscriberTopic)) + { + out.networkOrTopic = topic; + } + if (const char* mediaType = findValue(Discovery::supplKeyDataSubscriberMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyDataSubscriberSubLabels); + } + else if (controllerType == Discovery::controllerTypeRpcClient) + { + out.serviceKind = SilKit_Experimental_ServiceKind_RpcClient; + if (const char* functionName = findValue(Discovery::supplKeyRpcClientFunctionName)) + { + out.networkOrTopic = functionName; + } + if (const char* mediaType = findValue(Discovery::supplKeyRpcClientMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyRpcClientLabels); + } + else if (controllerType == Discovery::controllerTypeRpcServer) + { + out.serviceKind = SilKit_Experimental_ServiceKind_RpcServer; + if (const char* functionName = findValue(Discovery::supplKeyRpcServerFunctionName)) + { + out.networkOrTopic = functionName; + } + if (const char* mediaType = findValue(Discovery::supplKeyRpcServerMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyRpcServerLabels); + } + else + { + // Infrastructure / internal controllers (ServiceDiscovery, SystemMonitor, lifecycle, metrics, + // DataSubscriberInternal, RpcServerInternal, ...) are not user-facing and are not reported. + return false; + } + + return true; } } // namespace @@ -71,99 +221,27 @@ try ASSERT_VALID_POINTER_PARAMETER(serviceDiscovery); ASSERT_VALID_HANDLER_PARAMETER(handler); - auto* cppServiceDiscovery = reinterpret_cast(serviceDiscovery); + auto* cppServiceDiscovery = reinterpret_cast(serviceDiscovery); cppServiceDiscovery->RegisterServiceDiscoveryHandler( - [handler, context](SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type, + [handler, context](Discovery::ServiceDiscoveryEvent::Type type, const SilKit::Core::ServiceDescriptor& serviceDescriptor) { - handler(context, ToC(type), - reinterpret_cast(&serviceDescriptor)); + // This runs on the SIL Kit IO worker thread. Exceptions must never propagate into SIL Kit internals. + try + { + SilKit_Experimental_ServiceDescriptor cDescriptor; + LabelStorage storage; + if (!ClassifyAndFill(serviceDescriptor, cDescriptor, storage)) + { + return; + } + handler(context, ToC(type), &cDescriptor); + } + catch (...) + { + } }); return SilKit_ReturnCode_SUCCESS; } CAPI_CATCH_EXCEPTIONS - - -SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetParticipantName( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outParticipantName) -try -{ - ASSERT_VALID_POINTER_PARAMETER(serviceDescriptor); - ASSERT_VALID_OUT_PARAMETER(outParticipantName); - - *outParticipantName = FromC(serviceDescriptor)->GetParticipantName().c_str(); - - return SilKit_ReturnCode_SUCCESS; -} -CAPI_CATCH_EXCEPTIONS - - -SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetServiceName( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outServiceName) -try -{ - ASSERT_VALID_POINTER_PARAMETER(serviceDescriptor); - ASSERT_VALID_OUT_PARAMETER(outServiceName); - - *outServiceName = FromC(serviceDescriptor)->GetServiceName().c_str(); - - return SilKit_ReturnCode_SUCCESS; -} -CAPI_CATCH_EXCEPTIONS - - -SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetNetworkName( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char** outNetworkName) -try -{ - ASSERT_VALID_POINTER_PARAMETER(serviceDescriptor); - ASSERT_VALID_OUT_PARAMETER(outNetworkName); - - *outNetworkName = FromC(serviceDescriptor)->GetNetworkName().c_str(); - - return SilKit_ReturnCode_SUCCESS; -} -CAPI_CATCH_EXCEPTIONS - - -SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetServiceType( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, SilKit_Experimental_ServiceType* outServiceType) -try -{ - ASSERT_VALID_POINTER_PARAMETER(serviceDescriptor); - ASSERT_VALID_OUT_PARAMETER(outServiceType); - - *outServiceType = static_cast(FromC(serviceDescriptor)->GetServiceType()); - - return SilKit_ReturnCode_SUCCESS; -} -CAPI_CATCH_EXCEPTIONS - - -SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem( - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor, const char* key, const char** outValue, - SilKit_Bool* outHasValue) -try -{ - ASSERT_VALID_POINTER_PARAMETER(serviceDescriptor); - ASSERT_VALID_POINTER_PARAMETER(key); - ASSERT_VALID_OUT_PARAMETER(outValue); - ASSERT_VALID_OUT_PARAMETER(outHasValue); - - const auto& supplementalData = FromC(serviceDescriptor)->GetSupplementalDataRef(); - const auto it = supplementalData.find(key); - if (it == supplementalData.end()) - { - *outValue = nullptr; - *outHasValue = SilKit_False; - } - else - { - *outValue = it->second.c_str(); - *outHasValue = SilKit_True; - } - - return SilKit_ReturnCode_SUCCESS; -} -CAPI_CATCH_EXCEPTIONS diff --git a/SilKit/source/capi/Test_CapiInterfaces.cpp b/SilKit/source/capi/Test_CapiInterfaces.cpp index 360657ec7..1527aa351 100644 --- a/SilKit/source/capi/Test_CapiInterfaces.cpp +++ b/SilKit/source/capi/Test_CapiInterfaces.cpp @@ -64,6 +64,7 @@ constexpr uint64_t allSilkidIds[] = { SilKit_LifecycleConfiguration_STRUCT_VERSION, SilKit_WorkflowConfiguration_STRUCT_VERSION, SilKit_ParticipantConnectionInformation_STRUCT_VERSION, + SilKit_Experimental_ServiceDescriptor_STRUCT_VERSION, SilKit_Experimental_EventReceivers_STRUCT_VERSION, SilKit_Experimental_SimulatedNetworkFunctions_STRUCT_VERSION, SilKit_Experimental_SimulatedCanControllerFunctions_STRUCT_VERSION, diff --git a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp index 1d498ef46..4d02c1e2b 100644 --- a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp +++ b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp @@ -2,69 +2,80 @@ // // SPDX-License-Identifier: MIT +#include +#include +#include + #include "gtest/gtest.h" #include "gmock/gmock.h" #include "silkit/capi/SilKit.h" +#include "silkit/services/datatypes.hpp" #include "core/mock/participant/MockParticipant.hpp" #include "core/internal/ServiceDescriptor.hpp" +#include "core/internal/ServiceConfigKeys.hpp" +#include "config/YamlParser.hpp" namespace { using SilKit::Core::Tests::DummyParticipant; +using SilKit::Core::ServiceDescriptor; +using SilKit::Core::ServiceType; using ServiceDiscoveryEvent = SilKit::Core::Discovery::ServiceDiscoveryEvent; +namespace Discovery = SilKit::Core::Discovery; + +struct CapturedLabel +{ + std::string key; + std::string value; + SilKit_LabelKind kind; +}; -// Captures, inside the handler invocation, everything the C accessors return. Copying into owned strings here also -// verifies that the descriptor handle and the returned pointers are valid for the duration of the callback. +// Captures, inside the handler invocation, everything the public struct carries. Copying into owned storage here also +// verifies that the struct and all its borrowed pointers are valid for the duration of the callback. struct CallbackData { int callCount{0}; SilKit_Experimental_ServiceDiscoveryEvent_Type lastType{ SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid}; + SilKit_Experimental_ServiceKind serviceKind{SilKit_Experimental_ServiceKind_Undefined}; std::string participantName; std::string serviceName; std::string networkName; - SilKit_Experimental_ServiceType serviceType{SilKit_Experimental_ServiceType_Undefined}; - bool hasTopic{false}; - std::string topic; + std::string networkOrTopic; + std::string mediaType; + std::vector labels; }; -void SilKitCALL TestDiscoveryHandler(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type type, - const SilKit_Experimental_ServiceDescriptor* serviceDescriptor) +void SilKitCALL CapturingHandler(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor) { auto* data = static_cast(context); data->callCount += 1; data->lastType = type; - - const char* str = nullptr; - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetParticipantName(serviceDescriptor, &str), - SilKit_ReturnCode_SUCCESS); - data->participantName = str; - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceName(serviceDescriptor, &str), - SilKit_ReturnCode_SUCCESS); - data->serviceName = str; - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetNetworkName(serviceDescriptor, &str), - SilKit_ReturnCode_SUCCESS); - data->networkName = str; - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceType(serviceDescriptor, &data->serviceType), - SilKit_ReturnCode_SUCCESS); - - SilKit_Bool hasValue = SilKit_False; - const char* value = nullptr; - EXPECT_EQ( - SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(serviceDescriptor, "topic", &value, &hasValue), - SilKit_ReturnCode_SUCCESS); - data->hasTopic = (hasValue == SilKit_True); - if (data->hasTopic) + data->serviceKind = serviceDescriptor->serviceKind; + data->participantName = serviceDescriptor->participantName; + data->serviceName = serviceDescriptor->serviceName; + data->networkName = serviceDescriptor->networkName; + data->networkOrTopic = serviceDescriptor->networkOrTopic; + data->mediaType = serviceDescriptor->mediaType; + data->labels.clear(); + for (size_t i = 0; i < serviceDescriptor->labelList.numLabels; ++i) { - data->topic = value; + const auto& label = serviceDescriptor->labelList.labels[i]; + data->labels.push_back({label.key, label.value, label.kind}); } } -void SilKitCALL NoopDiscoveryHandler(void* /*context*/, SilKit_Experimental_ServiceDiscoveryEvent_Type /*type*/, - const SilKit_Experimental_ServiceDescriptor* /*serviceDescriptor*/) +void SilKitCALL NoopHandler(void* /*context*/, SilKit_Experimental_ServiceDiscoveryEvent_Type /*type*/, + const SilKit_Experimental_ServiceDescriptor* /*serviceDescriptor*/) +{ +} + +auto SerializeLabels(const std::vector& labels) -> std::string { + return SilKit::Config::Serialize(labels); } class Test_CapiServiceDiscovery : public testing::Test @@ -72,41 +83,50 @@ class Test_CapiServiceDiscovery : public testing::Test public: DummyParticipant mockParticipant; - // Build a populated descriptor for the accessor tests. - static SilKit::Core::ServiceDescriptor MakeDescriptor() + static ServiceDescriptor MakeController(const std::string& controllerType, const std::string& networkName, + const std::string& serviceName) { - SilKit::Core::ServiceDescriptor descriptor; + ServiceDescriptor descriptor; descriptor.SetParticipantNameAndComputeId("ParticipantA"); - descriptor.SetServiceName("MyPublisher"); - descriptor.SetNetworkName("NetworkX"); - descriptor.SetServiceType(SilKit::Core::ServiceType::Controller); - descriptor.SetSupplementalDataItem("topic", "TopicA"); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(networkName); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetSupplementalDataItem(Discovery::controllerType, controllerType); return descriptor; } - static const SilKit_Experimental_ServiceDescriptor* AsC(const SilKit::Core::ServiceDescriptor& descriptor) + // Registers the capturing C handler and returns the internal handler the C API installed on the service discovery. + SilKit::Core::Discovery::ServiceDiscoveryHandler InstallHandler(CallbackData& data) { - return reinterpret_cast(&descriptor); + SilKit::Core::Discovery::ServiceDiscoveryHandler capturedHandler; + EXPECT_CALL(mockParticipant.mockServiceDiscovery, RegisterServiceDiscoveryHandler(testing::_)) + .WillOnce(testing::SaveArg<0>(&capturedHandler)); + + SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), + SilKit_ReturnCode_SUCCESS); + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, &data, + &CapturingHandler), + SilKit_ReturnCode_SUCCESS); + EXPECT_TRUE(static_cast(capturedHandler)); + return capturedHandler; } }; TEST_F(Test_CapiServiceDiscovery, create_returns_service_discovery) { SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; - const auto returnCode = - SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant); - EXPECT_EQ(returnCode, SilKit_ReturnCode_SUCCESS); + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), + SilKit_ReturnCode_SUCCESS); EXPECT_NE(serviceDiscovery, nullptr); } TEST_F(Test_CapiServiceDiscovery, create_bad_parameters) { SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; - EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(nullptr, (SilKit_Participant*)&mockParticipant), SilKit_ReturnCode_BADPARAMETER); - EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, nullptr), - SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, nullptr), SilKit_ReturnCode_BADPARAMETER); } TEST_F(Test_CapiServiceDiscovery, set_handler_bad_parameters) @@ -114,107 +134,141 @@ TEST_F(Test_CapiServiceDiscovery, set_handler_bad_parameters) SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), SilKit_ReturnCode_SUCCESS); - - EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(nullptr, nullptr, &NoopDiscoveryHandler), + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(nullptr, nullptr, &NoopHandler), SilKit_ReturnCode_BADPARAMETER); EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, nullptr, nullptr), SilKit_ReturnCode_BADPARAMETER); } -TEST_F(Test_CapiServiceDiscovery, set_handler_registers_and_forwards_events) +TEST_F(Test_CapiServiceDiscovery, reports_can_controller) { - SilKit::Core::Discovery::ServiceDiscoveryHandler capturedHandler; - EXPECT_CALL(mockParticipant.mockServiceDiscovery, RegisterServiceDiscoveryHandler(testing::_)) - .WillOnce(testing::SaveArg<0>(&capturedHandler)); - - SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; - ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), - SilKit_ReturnCode_SUCCESS); - CallbackData data; - ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, &data, - &TestDiscoveryHandler), - SilKit_ReturnCode_SUCCESS); - ASSERT_TRUE(static_cast(capturedHandler)); + auto handler = InstallHandler(data); - const auto descriptor = MakeDescriptor(); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeController(Discovery::controllerTypeCan, "CAN1", "Can1")); - capturedHandler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); EXPECT_EQ(data.callCount, 1); EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_CanController); EXPECT_EQ(data.participantName, "ParticipantA"); - EXPECT_EQ(data.serviceName, "MyPublisher"); - EXPECT_EQ(data.networkName, "NetworkX"); - EXPECT_EQ(data.serviceType, SilKit_Experimental_ServiceType_Controller); - EXPECT_TRUE(data.hasTopic); - EXPECT_EQ(data.topic, "TopicA"); - - capturedHandler(ServiceDiscoveryEvent::Type::ServiceRemoved, descriptor); - EXPECT_EQ(data.callCount, 2); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.serviceName, "Can1"); + EXPECT_EQ(data.networkName, "CAN1"); + EXPECT_EQ(data.networkOrTopic, "CAN1"); // bus controller: join key is the network name + EXPECT_EQ(data.mediaType, ""); + EXPECT_TRUE(data.labels.empty()); } -TEST_F(Test_CapiServiceDiscovery, descriptor_accessors) +TEST_F(Test_CapiServiceDiscovery, reports_data_publisher_with_decoded_metadata) { - const auto descriptor = MakeDescriptor(); - const auto* cDescriptor = AsC(descriptor); + CallbackData data; + auto handler = InstallHandler(data); - const char* str = nullptr; - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetParticipantName(cDescriptor, &str), SilKit_ReturnCode_SUCCESS); - EXPECT_STREQ(str, "ParticipantA"); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceName(cDescriptor, &str), SilKit_ReturnCode_SUCCESS); - EXPECT_STREQ(str, "MyPublisher"); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetNetworkName(cDescriptor, &str), SilKit_ReturnCode_SUCCESS); - EXPECT_STREQ(str, "NetworkX"); + auto descriptor = MakeController(Discovery::controllerTypeDataPublisher, "pub-uuid-1234", "MyPublisher"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "TopicA"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherMediaType, "application/json"); + descriptor.SetSupplementalDataItem( + Discovery::supplKeyDataPublisherPubLabels, + SerializeLabels({{"kA", "vA", SilKit::Services::MatchingLabel::Kind::Mandatory}, + {"kB", "vB", SilKit::Services::MatchingLabel::Kind::Optional}})); - SilKit_Experimental_ServiceType serviceType = SilKit_Experimental_ServiceType_Undefined; - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceType(cDescriptor, &serviceType), - SilKit_ReturnCode_SUCCESS); - EXPECT_EQ(serviceType, SilKit_Experimental_ServiceType_Controller); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); - SilKit_Bool hasValue = SilKit_False; - const char* value = nullptr; - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(cDescriptor, "topic", &value, &hasValue), - SilKit_ReturnCode_SUCCESS); - EXPECT_EQ(hasValue, SilKit_True); - EXPECT_STREQ(value, "TopicA"); - - // Absent key is not an error: SUCCESS with hasValue == SilKit_False and value == nullptr. - EXPECT_EQ( - SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(cDescriptor, "missing", &value, &hasValue), - SilKit_ReturnCode_SUCCESS); - EXPECT_EQ(hasValue, SilKit_False); - EXPECT_EQ(value, nullptr); + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); + EXPECT_EQ(data.networkName, "pub-uuid-1234"); + EXPECT_EQ(data.networkOrTopic, "TopicA"); // pub/sub: join key is the topic + EXPECT_EQ(data.mediaType, "application/json"); + ASSERT_EQ(data.labels.size(), 2u); + EXPECT_EQ(data.labels[0].key, "kA"); + EXPECT_EQ(data.labels[0].value, "vA"); + EXPECT_EQ(data.labels[0].kind, SilKit_LabelKind_Mandatory); + EXPECT_EQ(data.labels[1].key, "kB"); + EXPECT_EQ(data.labels[1].kind, SilKit_LabelKind_Optional); } -TEST_F(Test_CapiServiceDiscovery, descriptor_accessors_bad_parameters) +TEST_F(Test_CapiServiceDiscovery, reports_rpc_client_with_function_name) { - const auto descriptor = MakeDescriptor(); - const auto* cDescriptor = AsC(descriptor); + CallbackData data; + auto handler = InstallHandler(data); - const char* str = nullptr; - SilKit_Experimental_ServiceType serviceType = SilKit_Experimental_ServiceType_Undefined; - SilKit_Bool hasValue = SilKit_False; - const char* value = nullptr; + auto descriptor = MakeController(Discovery::controllerTypeRpcClient, "rpc-uuid-9", "MyClient"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientFunctionName, "Add"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientMediaType, "application/octet-stream"); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetParticipantName(nullptr, &str), SilKit_ReturnCode_BADPARAMETER); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetParticipantName(cDescriptor, nullptr), - SilKit_ReturnCode_BADPARAMETER); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceName(nullptr, &str), SilKit_ReturnCode_BADPARAMETER); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetNetworkName(nullptr, &str), SilKit_ReturnCode_BADPARAMETER); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceType(nullptr, &serviceType), - SilKit_ReturnCode_BADPARAMETER); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetServiceType(cDescriptor, nullptr), - SilKit_ReturnCode_BADPARAMETER); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(nullptr, "topic", &value, &hasValue), - SilKit_ReturnCode_BADPARAMETER); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(cDescriptor, nullptr, &value, &hasValue), - SilKit_ReturnCode_BADPARAMETER); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(cDescriptor, "topic", nullptr, &hasValue), - SilKit_ReturnCode_BADPARAMETER); - EXPECT_EQ(SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(cDescriptor, "topic", &value, nullptr), - SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_RpcClient); + EXPECT_EQ(data.networkOrTopic, "Add"); // rpc: join key is the function name + EXPECT_EQ(data.mediaType, "application/octet-stream"); +} + +TEST_F(Test_CapiServiceDiscovery, reports_network_link) +{ + CallbackData data; + auto handler = InstallHandler(data); + + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ParticipantA"); + descriptor.SetServiceName("LinkService"); + descriptor.SetNetworkName("CAN1"); + descriptor.SetServiceType(ServiceType::Link); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); + + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_NetworkLink); + EXPECT_EQ(data.networkName, "CAN1"); + EXPECT_EQ(data.networkOrTopic, "CAN1"); +} + +TEST_F(Test_CapiServiceDiscovery, reports_removal) +{ + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeController(Discovery::controllerTypeLin, "LIN1", "Lin1")); + + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_LinController); +} + +TEST_F(Test_CapiServiceDiscovery, filters_infrastructure_and_internal_services) +{ + CallbackData data; + auto handler = InstallHandler(data); + + // Infrastructure controller (SystemMonitor) is not user-facing. + handler(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeSystemMonitor, "default", "SystemMonitor")); + // Internal pub/sub helper is not user-facing. + handler(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeDataSubscriberInternal, "sub-uuid", "SubInternal")); + + // A controller without a controller.type supplemental entry is skipped, too. + ServiceDescriptor noControllerType; + noControllerType.SetParticipantNameAndComputeId("ParticipantA"); + noControllerType.SetServiceName("Mystery"); + noControllerType.SetServiceType(ServiceType::Controller); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, noControllerType); + + EXPECT_EQ(data.callCount, 0); +} + +TEST_F(Test_CapiServiceDiscovery, malformed_labels_are_swallowed) +{ + CallbackData data; + auto handler = InstallHandler(data); + + auto descriptor = MakeController(Discovery::controllerTypeDataPublisher, "pub-uuid", "MyPublisher"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "TopicA"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherPubLabels, "{ this is : not [ valid"); + + // Must not throw across the FFI boundary; the service is still reported. + EXPECT_NO_THROW(handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor)); + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); + EXPECT_EQ(data.networkOrTopic, "TopicA"); } } // namespace diff --git a/SilKit/source/capi/Test_CapiSymbols.cpp b/SilKit/source/capi/Test_CapiSymbols.cpp index a9cc3c163..de06fafe1 100644 --- a/SilKit/source/capi/Test_CapiSymbols.cpp +++ b/SilKit/source/capi/Test_CapiSymbols.cpp @@ -131,11 +131,6 @@ TEST(Test_CapiSymbols, DISABLED_link_all_public_symbols) nullptr); (void)SilKit_Experimental_ServiceDiscovery_Create(nullptr, nullptr); (void)SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(nullptr, nullptr, nullptr); - (void)SilKit_Experimental_ServiceDescriptor_GetParticipantName(nullptr, nullptr); - (void)SilKit_Experimental_ServiceDescriptor_GetServiceName(nullptr, nullptr); - (void)SilKit_Experimental_ServiceDescriptor_GetNetworkName(nullptr, nullptr); - (void)SilKit_Experimental_ServiceDescriptor_GetServiceType(nullptr, nullptr); - (void)SilKit_Experimental_ServiceDescriptor_GetSupplementalDataItem(nullptr, nullptr, nullptr, nullptr); } } // namespace From 6a977e348c438208261b7ea9bc05e615d4fa18e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Wed, 15 Jul 2026 16:26:41 +0200 Subject: [PATCH 03/10] fixup! capi: refine service discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit drop UUID and other internals. only provide primaryIdentifier as handle key Signed-off-by: Marius Börschig --- SilKit/include/silkit/capi/Experimental.h | 10 ++++------ SilKit/source/capi/CapiExperimental.cpp | 11 +++++------ .../source/capi/Test_CapiServiceDiscovery.cpp | 19 +++++++------------ 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/SilKit/include/silkit/capi/Experimental.h b/SilKit/include/silkit/capi/Experimental.h index 48b441e12..32ca191d7 100644 --- a/SilKit/include/silkit/capi/Experimental.h +++ b/SilKit/include/silkit/capi/Experimental.h @@ -65,12 +65,10 @@ typedef struct const char* serviceName; //! The kind of service. SilKit_Experimental_ServiceKind serviceKind; - //! Raw network/link identifier. The user-facing network name for bus controllers (e.g. "CAN1"); a generated id or - //! "default" for pub/sub and RPC. - const char* networkName; - //! Convenience join key for visualization: the network name for bus controllers and links, the topic for - //! pub/sub, and the function name for RPC. - const char* networkOrTopic; + //! The primary, user-facing identifier of the service: the network name for bus controllers (e.g. "CAN1") and + //! network links, the topic for pub/sub, and the function name for RPC. Suitable as a display / join key for + //! visualization and tooling. + const char* primaryIdentifier; //! Media type for pub/sub and RPC services; empty string when not applicable. const char* mediaType; //! Decoded matching labels for pub/sub and RPC services; empty for bus controllers and links. diff --git a/SilKit/source/capi/CapiExperimental.cpp b/SilKit/source/capi/CapiExperimental.cpp index 4b5fcae3d..f8684ffc2 100644 --- a/SilKit/source/capi/CapiExperimental.cpp +++ b/SilKit/source/capi/CapiExperimental.cpp @@ -71,8 +71,7 @@ auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, out); out.participantName = serviceDescriptor.GetParticipantName().c_str(); out.serviceName = serviceDescriptor.GetServiceName().c_str(); - out.networkName = serviceDescriptor.GetNetworkName().c_str(); - out.networkOrTopic = serviceDescriptor.GetNetworkName().c_str(); + out.primaryIdentifier = serviceDescriptor.GetNetworkName().c_str(); out.mediaType = ""; // Network links carry no controller type and are reported as-is. @@ -138,7 +137,7 @@ auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, out.serviceKind = SilKit_Experimental_ServiceKind_DataPublisher; if (const char* topic = findValue(Discovery::supplKeyDataPublisherTopic)) { - out.networkOrTopic = topic; + out.primaryIdentifier = topic; } if (const char* mediaType = findValue(Discovery::supplKeyDataPublisherMediaType)) { @@ -151,7 +150,7 @@ auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, out.serviceKind = SilKit_Experimental_ServiceKind_DataSubscriber; if (const char* topic = findValue(Discovery::supplKeyDataSubscriberTopic)) { - out.networkOrTopic = topic; + out.primaryIdentifier = topic; } if (const char* mediaType = findValue(Discovery::supplKeyDataSubscriberMediaType)) { @@ -164,7 +163,7 @@ auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, out.serviceKind = SilKit_Experimental_ServiceKind_RpcClient; if (const char* functionName = findValue(Discovery::supplKeyRpcClientFunctionName)) { - out.networkOrTopic = functionName; + out.primaryIdentifier = functionName; } if (const char* mediaType = findValue(Discovery::supplKeyRpcClientMediaType)) { @@ -177,7 +176,7 @@ auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, out.serviceKind = SilKit_Experimental_ServiceKind_RpcServer; if (const char* functionName = findValue(Discovery::supplKeyRpcServerFunctionName)) { - out.networkOrTopic = functionName; + out.primaryIdentifier = functionName; } if (const char* mediaType = findValue(Discovery::supplKeyRpcServerMediaType)) { diff --git a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp index 4d02c1e2b..b0c723ad2 100644 --- a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp +++ b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp @@ -42,8 +42,7 @@ struct CallbackData SilKit_Experimental_ServiceKind serviceKind{SilKit_Experimental_ServiceKind_Undefined}; std::string participantName; std::string serviceName; - std::string networkName; - std::string networkOrTopic; + std::string primaryIdentifier; std::string mediaType; std::vector labels; }; @@ -57,8 +56,7 @@ void SilKitCALL CapturingHandler(void* context, SilKit_Experimental_ServiceDisco data->serviceKind = serviceDescriptor->serviceKind; data->participantName = serviceDescriptor->participantName; data->serviceName = serviceDescriptor->serviceName; - data->networkName = serviceDescriptor->networkName; - data->networkOrTopic = serviceDescriptor->networkOrTopic; + data->primaryIdentifier = serviceDescriptor->primaryIdentifier; data->mediaType = serviceDescriptor->mediaType; data->labels.clear(); for (size_t i = 0; i < serviceDescriptor->labelList.numLabels; ++i) @@ -152,8 +150,7 @@ TEST_F(Test_CapiServiceDiscovery, reports_can_controller) EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_CanController); EXPECT_EQ(data.participantName, "ParticipantA"); EXPECT_EQ(data.serviceName, "Can1"); - EXPECT_EQ(data.networkName, "CAN1"); - EXPECT_EQ(data.networkOrTopic, "CAN1"); // bus controller: join key is the network name + EXPECT_EQ(data.primaryIdentifier, "CAN1"); // bus controller: identifier is the network name EXPECT_EQ(data.mediaType, ""); EXPECT_TRUE(data.labels.empty()); } @@ -175,8 +172,7 @@ TEST_F(Test_CapiServiceDiscovery, reports_data_publisher_with_decoded_metadata) EXPECT_EQ(data.callCount, 1); EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); - EXPECT_EQ(data.networkName, "pub-uuid-1234"); - EXPECT_EQ(data.networkOrTopic, "TopicA"); // pub/sub: join key is the topic + EXPECT_EQ(data.primaryIdentifier, "TopicA"); // pub/sub: identifier is the topic EXPECT_EQ(data.mediaType, "application/json"); ASSERT_EQ(data.labels.size(), 2u); EXPECT_EQ(data.labels[0].key, "kA"); @@ -198,7 +194,7 @@ TEST_F(Test_CapiServiceDiscovery, reports_rpc_client_with_function_name) handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_RpcClient); - EXPECT_EQ(data.networkOrTopic, "Add"); // rpc: join key is the function name + EXPECT_EQ(data.primaryIdentifier, "Add"); // rpc: identifier is the function name EXPECT_EQ(data.mediaType, "application/octet-stream"); } @@ -217,8 +213,7 @@ TEST_F(Test_CapiServiceDiscovery, reports_network_link) EXPECT_EQ(data.callCount, 1); EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_NetworkLink); - EXPECT_EQ(data.networkName, "CAN1"); - EXPECT_EQ(data.networkOrTopic, "CAN1"); + EXPECT_EQ(data.primaryIdentifier, "CAN1"); } TEST_F(Test_CapiServiceDiscovery, reports_removal) @@ -268,7 +263,7 @@ TEST_F(Test_CapiServiceDiscovery, malformed_labels_are_swallowed) EXPECT_NO_THROW(handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor)); EXPECT_EQ(data.callCount, 1); EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); - EXPECT_EQ(data.networkOrTopic, "TopicA"); + EXPECT_EQ(data.primaryIdentifier, "TopicA"); } } // namespace From 4439ee804ff96b90a3d5ae7a705f13a672af48e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Wed, 15 Jul 2026 17:01:10 +0200 Subject: [PATCH 04/10] fixup! fixup! capi: refine service discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add validator integration testcase for matchin pub + sub Signed-off-by: Marius Börschig --- SilKit/IntegrationTests/CMakeLists.txt | 4 + .../ITest_CapiServiceDiscovery.cpp | 346 ++++++++++++++++++ 2 files changed, 350 insertions(+) create mode 100644 SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp diff --git a/SilKit/IntegrationTests/CMakeLists.txt b/SilKit/IntegrationTests/CMakeLists.txt index 0eb1b3a59..fbfd344f6 100644 --- a/SilKit/IntegrationTests/CMakeLists.txt +++ b/SilKit/IntegrationTests/CMakeLists.txt @@ -177,6 +177,10 @@ add_silkit_test_to_executable(SilKitIntegrationTests SOURCES ITest_PubHistory.cpp ) +add_silkit_test_to_executable(SilKitIntegrationTests + SOURCES ITest_CapiServiceDiscovery.cpp +) + # RPC add_silkit_test_to_executable(SilKitInternalIntegrationTests diff --git a/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp new file mode 100644 index 000000000..8c3a5460d --- /dev/null +++ b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp @@ -0,0 +1,346 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +// Integration test for the experimental service-discovery C API +// (SilKit_Experimental_ServiceDiscovery_*), driven against real participants over an +// in-process registry. +// +// It validates the motivating use case: an observer correlating subscribers to the publisher +// they are connected to. The authoritative connection edge inside SIL Kit is the per-publisher +// transport UUID, carried by the internal DataSubscriberInternal service. The C API deliberately +// does not expose that internal service or the UUID. +// +// The intended design (follow-up) keeps the public struct as-is and instead does bookkeeping at +// the C API boundary over the internal DataPublisher/DataSubscriberInternal announcements, +// synthesizing a user-facing DataSubscriber ServiceCreated/ServiceRemoved event *if and only if* +// a valid internal match exists (i.e. the subscriber is actually connected to a publisher). +// +// Therefore: +// - The enabled tests below cover the matched happy-path observable today AND after the +// follow-up (the gated events fire in a matched scenario either way). +// - The DISABLED_ tests are the validators for the follow-up: they assert the *gating* +// (a subscriber with no matching publisher must produce no event) and the *removal +// synthesis* (destroying the publisher disconnects the subscriber). They fail today and are +// the acceptance criteria for the follow-up. Run with --gtest_also_run_disabled_tests. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "silkit/SilKit.hpp" +#include "silkit/capi/SilKit.h" +#include "silkit/config/IParticipantConfiguration.hpp" +#include "silkit/services/pubsub/all.hpp" +#include "silkit/vendor/CreateSilKitRegistry.hpp" + +#include "gtest/gtest.h" + +namespace { + +using namespace std::chrono_literals; +using SilKit::Services::PubSub::PubSubSpec; +using SilKit::Services::PubSub::DataMessageEvent; +using SilKit::Services::PubSub::IDataSubscriber; + +constexpr auto kWaitTimeout = 10s; + +// A single service-discovery event as observed through the C API (all borrowed C data copied +// out, since the descriptor and its pointers are only valid for the duration of the callback). +struct ObservedLabel +{ + std::string key; + std::string value; + SilKit_LabelKind kind; +}; + +struct ObservedEvent +{ + SilKit_Experimental_ServiceDiscoveryEvent_Type eventType; + SilKit_Experimental_ServiceKind serviceKind; + std::string participantName; + std::string serviceName; + std::string primaryIdentifier; + std::string mediaType; + std::vector labels; +}; + +// Shared state between the C callback thread and the test thread. +struct ObserverContext +{ + std::mutex mutex; + std::condition_variable cv; + std::vector events; +}; + +// The C trampoline registered with SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler. +// Copies the descriptor into the context and wakes any waiter. +void SilKitCALL OnServiceDiscovery(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type eventType, + const SilKit_Experimental_ServiceDescriptor* descriptor) +{ + auto* ctx = static_cast(context); + + ObservedEvent event{}; + event.eventType = eventType; + event.serviceKind = descriptor->serviceKind; + event.participantName = descriptor->participantName; + event.serviceName = descriptor->serviceName; + event.primaryIdentifier = descriptor->primaryIdentifier; + event.mediaType = descriptor->mediaType; + for (size_t i = 0; i < descriptor->labelList.numLabels; ++i) + { + const auto& label = descriptor->labelList.labels[i]; + event.labels.push_back({label.key, label.value, label.kind}); + } + + { + std::lock_guard lock{ctx->mutex}; + ctx->events.push_back(std::move(event)); + } + ctx->cv.notify_all(); +} + +class ITest_CapiServiceDiscovery : public testing::Test +{ +protected: + void SetUp() override + { + _registry = SilKit::Vendor::Vector::CreateSilKitRegistry( + SilKit::Config::ParticipantConfigurationFromString("")); + _registryUri = _registry->StartListening("silkit://127.0.0.1:0"); + + // The observer is created through the C API so we get a genuine SilKit_Participant* to + // pass to the experimental discovery API (no casting between the C and C++ worlds). + SilKit_ParticipantConfiguration* config{nullptr}; + ASSERT_EQ(SilKit_ParticipantConfiguration_FromString(&config, ""), SilKit_ReturnCode_SUCCESS); + ASSERT_EQ(SilKit_Participant_Create(&_observer, config, "Observer", _registryUri.c_str()), + SilKit_ReturnCode_SUCCESS); + SilKit_ParticipantConfiguration_Destroy(config); + + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&_serviceDiscovery, _observer), + SilKit_ReturnCode_SUCCESS); + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(_serviceDiscovery, &_ctx, + &OnServiceDiscovery), + SilKit_ReturnCode_SUCCESS); + } + + void TearDown() override + { + // The service discovery observer is owned by the participant; destroying the participant + // tears it down. Participants created by the individual tests are destroyed at the end of + // the test body (before this runs). _ctx outlives all of them. + if (_observer != nullptr) + { + SilKit_Participant_Destroy(_observer); + _observer = nullptr; + } + _registry.reset(); + } + + auto CreateParticipant(const std::string& name) -> std::unique_ptr + { + return SilKit::CreateParticipant(SilKit::Config::ParticipantConfigurationFromString(""), name, + _registryUri); + } + + // Counts observed events matching kind + type + primaryIdentifier. Caller must hold _ctx.mutex. + auto CountUnlocked(SilKit_Experimental_ServiceKind kind, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const std::string& primaryIdentifier) const -> size_t + { + return static_cast(std::count_if(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == kind && e.eventType == type && e.primaryIdentifier == primaryIdentifier; + })); + } + + auto Count(SilKit_Experimental_ServiceKind kind, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const std::string& primaryIdentifier) -> size_t + { + std::lock_guard lock{_ctx.mutex}; + return CountUnlocked(kind, type, primaryIdentifier); + } + + template + auto WaitFor(Predicate predicate, std::chrono::milliseconds timeout = kWaitTimeout) -> bool + { + std::unique_lock lock{_ctx.mutex}; + return _ctx.cv.wait_for(lock, timeout, [&] { return predicate(); }); + } + + // Returns a copy of the first observed event matching kind + type + primaryIdentifier. + auto FindEvent(SilKit_Experimental_ServiceKind kind, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const std::string& primaryIdentifier) -> ObservedEvent + { + std::lock_guard lock{_ctx.mutex}; + const auto it = std::find_if(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == kind && e.eventType == type && e.primaryIdentifier == primaryIdentifier; + }); + return it == _ctx.events.end() ? ObservedEvent{} : *it; + } + + std::unique_ptr _registry; + std::string _registryUri; + SilKit_Participant* _observer{nullptr}; + SilKit_Experimental_ServiceDiscovery* _serviceDiscovery{nullptr}; + ObserverContext _ctx; +}; + +// 1 publisher + 2 subscribers on the same topic. The observer must see the publisher and both +// subscribers with the right kind, primaryIdentifier (== topic), mediaType and labels. +// (In this matched scenario the events fire whether or not they are gated on a connection, so +// this stays green across the follow-up; the semantic then shifts from "created" to "connected".) +TEST_F(ITest_CapiServiceDiscovery, observer_sees_publisher_and_both_subscribers) +{ + const std::string topic{"T"}; + const std::string mediaType{"M"}; + + PubSubSpec spec{topic, mediaType}; + spec.AddLabel("K", "V", SilKit::Services::MatchingLabel::Kind::Mandatory); + + auto publisher = CreateParticipant("Pub"); + auto subscriber1 = CreateParticipant("Sub1"); + auto subscriber2 = CreateParticipant("Sub2"); + + publisher->CreateDataPublisher("PubCtrl", spec, 1); + subscriber1->CreateDataSubscriber("SubCtrl1", spec, [](IDataSubscriber*, const DataMessageEvent&) {}); + subscriber2->CreateDataSubscriber("SubCtrl2", spec, [](IDataSubscriber*, const DataMessageEvent&) {}); + + ASSERT_TRUE(WaitFor([&] { + return CountUnlocked(SilKit_Experimental_ServiceKind_DataPublisher, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic) + >= 1 + && CountUnlocked(SilKit_Experimental_ServiceKind_DataSubscriber, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic) + >= 2; + })) << "observer did not see the publisher and both subscribers within the timeout"; + + EXPECT_EQ(Count(SilKit_Experimental_ServiceKind_DataPublisher, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic), + 1u); + EXPECT_EQ(Count(SilKit_Experimental_ServiceKind_DataSubscriber, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic), + 2u); + + // The publisher descriptor must carry the topic as primaryIdentifier, plus mediaType + labels. + const auto publisherEvent = FindEvent(SilKit_Experimental_ServiceKind_DataPublisher, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic); + EXPECT_EQ(publisherEvent.primaryIdentifier, topic); + EXPECT_EQ(publisherEvent.mediaType, mediaType); + ASSERT_EQ(publisherEvent.labels.size(), 1u); + EXPECT_EQ(publisherEvent.labels[0].key, "K"); + EXPECT_EQ(publisherEvent.labels[0].value, "V"); + EXPECT_EQ(publisherEvent.labels[0].kind, SilKit_LabelKind_Mandatory); +} + +// Ground truth (independent of the C API): the two subscribers really connect to the publisher. +// The publisher uses history=1 and publishes once, so each subscriber receives the sample upon +// connecting, regardless of ordering. +TEST_F(ITest_CapiServiceDiscovery, both_subscribers_receive_published_data) +{ + const std::string topic{"T"}; + const std::string mediaType{"M"}; + PubSubSpec spec{topic, mediaType}; + + struct RecvLatch + { + std::mutex mutex; + std::condition_variable cv; + int count{0}; + void Hit() + { + { + std::lock_guard lock{mutex}; + ++count; + } + cv.notify_all(); + } + auto Wait(int expected, std::chrono::milliseconds timeout) -> bool + { + std::unique_lock lock{mutex}; + return cv.wait_for(lock, timeout, [&] { return count >= expected; }); + } + } latch; + + auto publisher = CreateParticipant("Pub"); + auto* dataPublisher = publisher->CreateDataPublisher("PubCtrl", spec, 1); + dataPublisher->Publish(std::vector{42}); + + auto subscriber1 = CreateParticipant("Sub1"); + auto subscriber2 = CreateParticipant("Sub2"); + subscriber1->CreateDataSubscriber("SubCtrl1", spec, [&](IDataSubscriber*, const DataMessageEvent&) { latch.Hit(); }); + subscriber2->CreateDataSubscriber("SubCtrl2", spec, [&](IDataSubscriber*, const DataMessageEvent&) { latch.Hit(); }); + + EXPECT_TRUE(latch.Wait(2, kWaitTimeout)) << "both subscribers should receive the published sample"; +} + +// VALIDATOR (follow-up): the gating discriminator. A subscriber whose topic has no publisher must +// NOT produce a DataSubscriber ServiceCreated event at the observer. Today the user-facing +// subscriber event fires on creation regardless, so this FAILS. It PASSES once the C API gates +// the event on a confirmed internal match. +TEST_F(ITest_CapiServiceDiscovery, DISABLED_no_subscriber_event_without_matching_publisher) +{ + const std::string lonelyTopic{"LonelyTopic"}; + const std::string otherTopic{"OtherTopic"}; + const std::string mediaType{"M"}; + + // A publisher on an unrelated topic gives us a deterministic "discovery has propagated" signal + // without ever matching the lonely subscriber. + auto publisher = CreateParticipant("Pub"); + publisher->CreateDataPublisher("PubCtrl", PubSubSpec{otherTopic, mediaType}, 0); + + auto subscriber = CreateParticipant("LonelySub"); + subscriber->CreateDataSubscriber("SubCtrl", PubSubSpec{lonelyTopic, mediaType}, + [](IDataSubscriber*, const DataMessageEvent&) {}); + + // Wait until discovery has demonstrably reached the observer (it sees the unrelated publisher). + ASSERT_TRUE(WaitFor([&] { + return CountUnlocked(SilKit_Experimental_ServiceKind_DataPublisher, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, otherTopic) + >= 1; + })) << "observer never saw the unrelated publisher"; + + // Grace period for any (erroneously emitted) subscriber event to arrive as well. + std::this_thread::sleep_for(500ms); + + EXPECT_EQ(Count(SilKit_Experimental_ServiceKind_DataSubscriber, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, lonelyTopic), + 0u) + << "an unmatched subscriber must not be reported as connected"; +} + +// VALIDATOR (follow-up): the removal-synthesis half. Start matched (observer sees the subscriber +// connected), then destroy the publisher; the observer must receive a synthesized DataSubscriber +// ServiceRemoved (disconnected) event. Today destroying the publisher does not remove the +// user-facing subscriber, so no such event is emitted and this FAILS. +TEST_F(ITest_CapiServiceDiscovery, DISABLED_subscriber_event_removed_when_publisher_leaves) +{ + const std::string topic{"T"}; + const std::string mediaType{"M"}; + PubSubSpec spec{topic, mediaType}; + + auto publisher = CreateParticipant("Pub"); + auto subscriber = CreateParticipant("Sub"); + publisher->CreateDataPublisher("PubCtrl", spec, 1); + subscriber->CreateDataSubscriber("SubCtrl", spec, [](IDataSubscriber*, const DataMessageEvent&) {}); + + ASSERT_TRUE(WaitFor([&] { + return CountUnlocked(SilKit_Experimental_ServiceKind_DataSubscriber, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic) + >= 1; + })) << "observer never saw the connected subscriber"; + + publisher.reset(); + + ASSERT_TRUE(WaitFor([&] { + return CountUnlocked(SilKit_Experimental_ServiceKind_DataSubscriber, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, topic) + >= 1; + })) << "observer should see the subscriber disconnected when the publisher leaves"; +} + +} // namespace From 5d0d37765371211708c3b83b7c73c5ab328bc294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Wed, 15 Jul 2026 17:27:58 +0200 Subject: [PATCH 05/10] synthesize subscribers based on internal services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- .../ITest_CapiServiceDiscovery.cpp | 51 +++-- SilKit/source/capi/CapiExperimental.cpp | 205 +++++++++++++++++- .../source/capi/Test_CapiServiceDiscovery.cpp | 115 ++++++++++ 3 files changed, 336 insertions(+), 35 deletions(-) diff --git a/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp index 8c3a5460d..aed5ad50d 100644 --- a/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp +++ b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include #include "silkit/SilKit.hpp" @@ -246,7 +245,7 @@ TEST_F(ITest_CapiServiceDiscovery, both_subscribers_receive_published_data) const std::string mediaType{"M"}; PubSubSpec spec{topic, mediaType}; - struct RecvLatch + struct ReceiveLatch { std::mutex mutex; std::condition_variable cv; @@ -278,34 +277,35 @@ TEST_F(ITest_CapiServiceDiscovery, both_subscribers_receive_published_data) EXPECT_TRUE(latch.Wait(2, kWaitTimeout)) << "both subscribers should receive the published sample"; } -// VALIDATOR (follow-up): the gating discriminator. A subscriber whose topic has no publisher must -// NOT produce a DataSubscriber ServiceCreated event at the observer. Today the user-facing -// subscriber event fires on creation regardless, so this FAILS. It PASSES once the C API gates -// the event on a confirmed internal match. -TEST_F(ITest_CapiServiceDiscovery, DISABLED_no_subscriber_event_without_matching_publisher) +// The gating discriminator. A subscriber whose topic has no publisher must NOT produce a +// DataSubscriber ServiceCreated event at the observer: the C API only reports a subscriber once it +// is connected to a publisher (a confirmed internal match). +// +// The barrier is deterministic (no wall-clock wait): the unmatched subscriber and a sentinel +// publisher are created, in that order, on the SAME participant. All of a participant's service +// announcements reach the observer over a single in-order connection, so the subscriber's +// announcement is delivered no later than the sentinel's. Once the observer has seen the sentinel +// publisher, it has necessarily already processed the subscriber's announcement -- so if the +// (removed) premature-report behavior were present, the event would already be recorded. +TEST_F(ITest_CapiServiceDiscovery, no_subscriber_event_without_matching_publisher) { const std::string lonelyTopic{"LonelyTopic"}; - const std::string otherTopic{"OtherTopic"}; + const std::string sentinelTopic{"SentinelTopic"}; const std::string mediaType{"M"}; - // A publisher on an unrelated topic gives us a deterministic "discovery has propagated" signal - // without ever matching the lonely subscriber. - auto publisher = CreateParticipant("Pub"); - publisher->CreateDataPublisher("PubCtrl", PubSubSpec{otherTopic, mediaType}, 0); + auto participant = CreateParticipant("LonelyParticipant"); - auto subscriber = CreateParticipant("LonelySub"); - subscriber->CreateDataSubscriber("SubCtrl", PubSubSpec{lonelyTopic, mediaType}, - [](IDataSubscriber*, const DataMessageEvent&) {}); + // Unmatched subscriber first, then the sentinel publisher (unrelated topic, so it never matches + // the subscriber) -- both on the same participant to get in-order delivery to the observer. + participant->CreateDataSubscriber("SubCtrl", PubSubSpec{lonelyTopic, mediaType}, + [](IDataSubscriber*, const DataMessageEvent&) {}); + participant->CreateDataPublisher("SentinelPub", PubSubSpec{sentinelTopic, mediaType}, 0); - // Wait until discovery has demonstrably reached the observer (it sees the unrelated publisher). ASSERT_TRUE(WaitFor([&] { return CountUnlocked(SilKit_Experimental_ServiceKind_DataPublisher, - SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, otherTopic) + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, sentinelTopic) >= 1; - })) << "observer never saw the unrelated publisher"; - - // Grace period for any (erroneously emitted) subscriber event to arrive as well. - std::this_thread::sleep_for(500ms); + })) << "observer never saw the sentinel publisher"; EXPECT_EQ(Count(SilKit_Experimental_ServiceKind_DataSubscriber, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, lonelyTopic), @@ -313,11 +313,10 @@ TEST_F(ITest_CapiServiceDiscovery, DISABLED_no_subscriber_event_without_matching << "an unmatched subscriber must not be reported as connected"; } -// VALIDATOR (follow-up): the removal-synthesis half. Start matched (observer sees the subscriber -// connected), then destroy the publisher; the observer must receive a synthesized DataSubscriber -// ServiceRemoved (disconnected) event. Today destroying the publisher does not remove the -// user-facing subscriber, so no such event is emitted and this FAILS. -TEST_F(ITest_CapiServiceDiscovery, DISABLED_subscriber_event_removed_when_publisher_leaves) +// The removal-synthesis half. Start matched (observer sees the subscriber connected), then destroy +// the publisher; the observer must receive a synthesized DataSubscriber ServiceRemoved +// (disconnected) event once the subscriber's last connection drops. +TEST_F(ITest_CapiServiceDiscovery, subscriber_event_removed_when_publisher_leaves) { const std::string topic{"T"}; const std::string mediaType{"M"}; diff --git a/SilKit/source/capi/CapiExperimental.cpp b/SilKit/source/capi/CapiExperimental.cpp index f8684ffc2..125256a5d 100644 --- a/SilKit/source/capi/CapiExperimental.cpp +++ b/SilKit/source/capi/CapiExperimental.cpp @@ -16,7 +16,11 @@ #include "config/YamlParser.hpp" +#include +#include +#include #include +#include #include namespace { @@ -194,6 +198,188 @@ auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, return true; } +// Identifies a service across the simulation: (participant name, service id). +using ServiceKey = std::pair; + +struct SubscriberEntry +{ + SilKit::Core::ServiceDescriptor descriptor; + bool haveDescriptor{false}; + std::size_t connections{0}; + bool announced{false}; +}; + +// A synthesized event, queued while the state lock is held and dispatched after it is released. +struct PendingEmission +{ + SilKit_Experimental_ServiceDiscoveryEvent_Type type; + SilKit::Core::ServiceDescriptor descriptor; +}; + +// Per-observer bookkeeping. Lives as long as the participant's service discovery, because it is +// captured by the registered handler (which is never unregistered). +// +// Semantics: a user-facing DataSubscriber is reported to the observer only while it has at least one +// confirmed connection to a publisher. SIL Kit creates exactly one internal DataSubscriberInternal +// service per matched publisher - its existence IS the confirmed match - carrying the parent +// DataSubscriber's service id. We ref-count these connections and synthesize a ServiceCreated when +// the first connection appears and a ServiceRemoved when the last one disappears. Internal services +// and the transport UUID are never exposed. All other kinds (publishers, RPC, bus, links) are +// reported on creation, unchanged. +// +// (RPC has an analogous RpcServerInternal edge but is intentionally out of scope here: RPC +// client/server are still reported on creation.) +struct ObserverState +{ + SilKit_Experimental_ServiceDiscoveryHandler_t handler; + void* context; + + std::mutex mutex; + std::map subscribers; + std::map connectionParent; // internal-subscriber id -> parent subscriber id +}; + +void Emit(ObserverState& state, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const SilKit::Core::ServiceDescriptor& descriptor) +{ + SilKit_Experimental_ServiceDescriptor descriptor; + LabelStorage storage; + if (ClassifyAndFill(descriptor, descriptor, storage)) + { + state.handler(state.context, type, &descriptor); + } +} + +// Handles a single internal discovery event. Subscriber-related events drive the connection +// bookkeeping and may synthesize user-facing DataSubscriber events; every other kind is forwarded +// as-is. Emissions are dispatched outside the lock so user code never runs while we hold it. +void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor) +{ + using EventType = Discovery::ServiceDiscoveryEvent::Type; + + std::string controllerType; + descriptor.GetSupplementalDataItem(Discovery::controllerType, controllerType); + + if (controllerType == Discovery::controllerTypeDataSubscriber) + { + const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; + std::vector emissions; + { + std::lock_guard lock{state.mutex}; + if (type == EventType::ServiceCreated) + { + auto& entry = state.subscribers[key]; + entry.descriptor = descriptor; + entry.haveDescriptor = true; + if (entry.connections > 0 && !entry.announced) + { + entry.announced = true; + emissions.push_back( + {SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, entry.descriptor}); + } + } + else if (type == EventType::ServiceRemoved) + { + const auto it = state.subscribers.find(key); + if (it != state.subscribers.end()) + { + if (it->second.announced) + { + emissions.push_back( + {SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, it->second.descriptor}); + } + state.subscribers.erase(it); + // Drop any still-open connections that referenced this subscriber. + for (auto cit = state.connectionParent.begin(); cit != state.connectionParent.end();) + { + cit = (cit->second == key) ? state.connectionParent.erase(cit) : std::next(cit); + } + } + } + } + for (const auto& emission : emissions) + { + Emit(state, emission.type, emission.descriptor); + } + return; + } + + if (controllerType == Discovery::controllerTypeDataSubscriberInternal) + { + const ServiceKey internalKey{descriptor.GetParticipantName(), descriptor.GetServiceId()}; + std::string parentIdStr; + if (!descriptor.GetSupplementalDataItem(Discovery::supplKeyDataSubscriberInternalParentServiceID, parentIdStr)) + { + return; + } + SilKit::Core::EndpointId parentServiceId{0}; + try + { + parentServiceId = static_cast(std::stoull(parentIdStr)); + } + catch (...) + { + return; + } + const ServiceKey parentKey{descriptor.GetParticipantName(), parentServiceId}; + + std::vector emissions; + { + std::lock_guard lock{state.mutex}; + if (type == EventType::ServiceCreated) + { + if (state.connectionParent.count(internalKey) == 0) + { + state.connectionParent[internalKey] = parentKey; + auto& entry = state.subscribers[parentKey]; + ++entry.connections; + if (entry.haveDescriptor && !entry.announced) + { + entry.announced = true; + emissions.push_back( + {SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, entry.descriptor}); + } + } + } + else if (type == EventType::ServiceRemoved) + { + const auto cit = state.connectionParent.find(internalKey); + if (cit != state.connectionParent.end()) + { + const auto storedParentKey = cit->second; + state.connectionParent.erase(cit); + const auto sit = state.subscribers.find(storedParentKey); + if (sit != state.subscribers.end() && sit->second.connections > 0) + { + --sit->second.connections; + if (sit->second.connections == 0 && sit->second.announced) + { + sit->second.announced = false; + emissions.push_back( + {SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, sit->second.descriptor}); + } + } + } + } + } + for (const auto& emission : emissions) + { + Emit(state, emission.type, emission.descriptor); + } + return; + } + + // Publishers, RPC clients/servers, bus controllers, network links, and anything else: reported on + // creation/removal exactly as before. + SilKit_Experimental_ServiceDescriptor descriptor; + LabelStorage storage; + if (ClassifyAndFill(descriptor, descriptor, storage)) + { + state.handler(state.context, ToC(type), &descriptor); + } +} + } // namespace @@ -222,19 +408,20 @@ try auto* cppServiceDiscovery = reinterpret_cast(serviceDiscovery); + // The observer state is captured by (and thus owned by) the registered handler, which lives as + // long as the participant's service discovery. It buffers pub/sub matches and synthesizes + // connection-gated DataSubscriber events (see ObserverState / HandleDiscoveryEvent). + auto state = std::make_shared(); + state->handler = handler; + state->context = context; + cppServiceDiscovery->RegisterServiceDiscoveryHandler( - [handler, context](Discovery::ServiceDiscoveryEvent::Type type, - const SilKit::Core::ServiceDescriptor& serviceDescriptor) { + [state](Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& serviceDescriptor) { // This runs on the SIL Kit IO worker thread. Exceptions must never propagate into SIL Kit internals. try { - SilKit_Experimental_ServiceDescriptor cDescriptor; - LabelStorage storage; - if (!ClassifyAndFill(serviceDescriptor, cDescriptor, storage)) - { - return; - } - handler(context, ToC(type), &cDescriptor); + HandleDiscoveryEvent(*state, type, serviceDescriptor); } catch (...) { diff --git a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp index b0c723ad2..160339f20 100644 --- a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp +++ b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp @@ -266,4 +266,119 @@ TEST_F(Test_CapiServiceDiscovery, malformed_labels_are_swallowed) EXPECT_EQ(data.primaryIdentifier, "TopicA"); } +// -------------------------------------------------------------------------------------------------- +// Connection-gated DataSubscriber synthesis: a user-facing DataSubscriber is reported only while it +// has at least one confirmed connection (a DataSubscriberInternal announcement whose parent-service +// id points back at it). Internal services and the transport UUID are never exposed. +// -------------------------------------------------------------------------------------------------- + +namespace { +// User-facing DataSubscriber with an explicit service id and topic (topic drives primaryIdentifier). +auto MakeUserSubscriber(SilKit::Core::EndpointId serviceId, const std::string& topic) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("SubParticipant"); + descriptor.SetServiceName("Sub"); + descriptor.SetNetworkName("default"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataSubscriber); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberTopic, topic); + return descriptor; +} + +// DataSubscriberInternal (a confirmed connection) whose parent is the user-facing subscriber above. +auto MakeInternalConnection(SilKit::Core::EndpointId serviceId, SilKit::Core::EndpointId parentServiceId) + -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("SubParticipant"); + descriptor.SetServiceName("SubInternal"); + descriptor.SetNetworkName("pub-uuid"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataSubscriberInternal); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberInternalParentServiceID, + std::to_string(parentServiceId)); + return descriptor; +} +} // namespace + +TEST_F(Test_CapiServiceDiscovery, subscriber_without_connection_is_not_reported) +{ + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + + EXPECT_EQ(data.callCount, 0); +} + +TEST_F(Test_CapiServiceDiscovery, subscriber_reported_when_connection_appears) +{ + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); + + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + EXPECT_EQ(data.primaryIdentifier, "T"); +} + +TEST_F(Test_CapiServiceDiscovery, subscriber_reported_when_connection_arrives_before_subscriber) +{ + CallbackData data; + auto handler = InstallHandler(data); + + // Reversed order: the connection is discovered before the user-facing subscriber. + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); + EXPECT_EQ(data.callCount, 0); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); +} + +TEST_F(Test_CapiServiceDiscovery, subscriber_removed_when_connection_removed) +{ + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); + ASSERT_EQ(data.callCount, 1); + + handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(200, 100)); + + EXPECT_EQ(data.callCount, 2); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); +} + +TEST_F(Test_CapiServiceDiscovery, subscriber_reported_once_for_multiple_connections) +{ + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + + // Two publishers => two internal connections for the same subscriber: only one ServiceCreated. + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(201, 100)); + EXPECT_EQ(data.callCount, 1); + + // Removing one connection keeps the subscriber connected (no event). + handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(200, 100)); + EXPECT_EQ(data.callCount, 1); + + // Removing the last connection reports the subscriber removed. + handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(201, 100)); + EXPECT_EQ(data.callCount, 2); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); +} + } // namespace From 37427707f23196ca74b2ec93a01f81e2341cb134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Thu, 16 Jul 2026 10:39:38 +0200 Subject: [PATCH 06/10] c++ hourglass on top of capi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- .../IntegrationTests/Hourglass/CMakeLists.txt | 7 + .../IntegrationTests/Hourglass/MockCapi.cpp | 16 ++ .../IntegrationTests/Hourglass/MockCapi.hpp | 9 + .../Test_HourglassServiceDiscovery.cpp | 161 ++++++++++++++++++ .../participant/ParticipantExtensions.ipp | 11 ++ .../serviceDiscovery/ServiceDiscovery.hpp | 111 ++++++++++++ .../detail/impl/participant/Participant.hpp | 11 ++ .../participant/ParticipantExtensions.hpp | 10 ++ .../serviceDiscovery/IServiceDiscovery.hpp | 38 +++++ .../ServiceDiscoveryDatatypes.hpp | 72 ++++++++ .../serviceDiscovery/string_utils.hpp | 87 ++++++++++ 11 files changed, 533 insertions(+) create mode 100644 SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp create mode 100644 SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp create mode 100644 SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp create mode 100644 SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp create mode 100644 SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp diff --git a/SilKit/IntegrationTests/Hourglass/CMakeLists.txt b/SilKit/IntegrationTests/Hourglass/CMakeLists.txt index d22ee24b2..76fa5e1ed 100644 --- a/SilKit/IntegrationTests/Hourglass/CMakeLists.txt +++ b/SilKit/IntegrationTests/Hourglass/CMakeLists.txt @@ -114,3 +114,10 @@ add_silkit_test_to_executable(SilKitHourglassTests LIBS S_Test_Hourglass ) + +add_silkit_test_to_executable(SilKitHourglassTests + SOURCES + Test_HourglassServiceDiscovery.cpp + LIBS + S_Test_Hourglass +) diff --git a/SilKit/IntegrationTests/Hourglass/MockCapi.cpp b/SilKit/IntegrationTests/Hourglass/MockCapi.cpp index ccedbf562..40e13c09b 100644 --- a/SilKit/IntegrationTests/Hourglass/MockCapi.cpp +++ b/SilKit/IntegrationTests/Hourglass/MockCapi.cpp @@ -748,6 +748,22 @@ extern "C" workflowConfiguration); } + // ServiceDiscovery + + SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_Create( + SilKit_Experimental_ServiceDiscovery** outServiceDiscovery, SilKit_Participant* participant) + { + return globalCapi->SilKit_Experimental_ServiceDiscovery_Create(outServiceDiscovery, participant); + } + + SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler( + SilKit_Experimental_ServiceDiscovery* serviceDiscovery, void* context, + SilKit_Experimental_ServiceDiscoveryHandler_t handler) + { + return globalCapi->SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, context, + handler); + } + // Participant SilKit_ReturnCode SilKitCALL SilKit_Participant_Create(SilKit_Participant** outParticipant, diff --git a/SilKit/IntegrationTests/Hourglass/MockCapi.hpp b/SilKit/IntegrationTests/Hourglass/MockCapi.hpp index 1b863ea8b..888b36e92 100644 --- a/SilKit/IntegrationTests/Hourglass/MockCapi.hpp +++ b/SilKit/IntegrationTests/Hourglass/MockCapi.hpp @@ -396,6 +396,15 @@ class MockCapi (SilKit_Experimental_SystemController * systemController, const SilKit_WorkflowConfiguration* workflowConfiguration)); + // ServiceDiscovery + + MOCK_METHOD(SilKit_ReturnCode, SilKit_Experimental_ServiceDiscovery_Create, + (SilKit_Experimental_ServiceDiscovery * *outServiceDiscovery, SilKit_Participant* participant)); + + MOCK_METHOD(SilKit_ReturnCode, SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler, + (SilKit_Experimental_ServiceDiscovery * serviceDiscovery, void* context, + SilKit_Experimental_ServiceDiscoveryHandler_t handler)); + // Participant MOCK_METHOD(SilKit_ReturnCode, SilKit_Participant_Create, diff --git a/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp b/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp new file mode 100644 index 000000000..e1e970d2b --- /dev/null +++ b/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "silkit/capi/SilKit.h" + +#include "silkit/SilKit.hpp" +#include "silkit/detail/impl/ThrowOnError.hpp" +#include "silkit/experimental/participant/ParticipantExtensions.hpp" +#include "silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp" +#include "silkit/experimental/serviceDiscovery/string_utils.hpp" + +#include "MockCapiTest.hpp" + +#include +#include + +namespace { + +using testing::_; +using testing::DoAll; +using testing::Return; +using testing::SaveArg; +using testing::SetArgPointee; + +namespace SD = SilKit::Experimental::ServiceDiscovery; +namespace Detail = SilKit::DETAIL_SILKIT_DETAIL_NAMESPACE_NAME; + +using ServiceDiscoveryWrapper = Detail::Impl::Experimental::ServiceDiscovery::ServiceDiscovery; + +class Test_HourglassServiceDiscovery : public SilKitHourglassTests::MockCapiTest +{ +public: + SilKit_Participant* mockParticipant{reinterpret_cast(uintptr_t(0x12345678))}; + SilKit_Experimental_ServiceDiscovery* mockServiceDiscovery{ + reinterpret_cast(uintptr_t(0x45362718))}; + + Test_HourglassServiceDiscovery() + { + ON_CALL(capi, SilKit_Experimental_ServiceDiscovery_Create(_, _)) + .WillByDefault(DoAll(SetArgPointee<0>(mockServiceDiscovery), Return(SilKit_ReturnCode_SUCCESS))); + } +}; + +TEST_F(Test_HourglassServiceDiscovery, SilKit_Experimental_ServiceDiscovery_Create) +{ + Detail::Impl::Participant participant{mockParticipant}; + + EXPECT_CALL(capi, SilKit_Experimental_ServiceDiscovery_Create(_, mockParticipant)); + + auto* serviceDiscovery = Detail::Experimental::Participant::CreateServiceDiscovery(&participant); + EXPECT_NE(serviceDiscovery, nullptr); +} + +TEST_F(Test_HourglassServiceDiscovery, SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler) +{ + ServiceDiscoveryWrapper serviceDiscovery{mockParticipant}; + + void* capturedContext{nullptr}; + SilKit_Experimental_ServiceDiscoveryHandler_t capturedHandler{nullptr}; + EXPECT_CALL(capi, SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(mockServiceDiscovery, _, _)) + .WillOnce(DoAll(SaveArg<1>(&capturedContext), SaveArg<2>(&capturedHandler), + Return(SilKit_ReturnCode_SUCCESS))); + + serviceDiscovery.SetServiceDiscoveryHandler([](SD::ServiceDiscoveryEventType, const SD::ServiceDescriptor&) {}); + + EXPECT_NE(capturedContext, nullptr); + EXPECT_NE(capturedHandler, nullptr); +} + +// The C trampoline installed by the wrapper must translate the borrowed C descriptor into an owned +// C++ ServiceDescriptor and dispatch it to the user's std::function. +TEST_F(Test_HourglassServiceDiscovery, service_discovery_handler_round_trip) +{ + ServiceDiscoveryWrapper serviceDiscovery{mockParticipant}; + + void* capturedContext{nullptr}; + SilKit_Experimental_ServiceDiscoveryHandler_t capturedHandler{nullptr}; + EXPECT_CALL(capi, SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(mockServiceDiscovery, _, _)) + .WillOnce(DoAll(SaveArg<1>(&capturedContext), SaveArg<2>(&capturedHandler), + Return(SilKit_ReturnCode_SUCCESS))); + + SD::ServiceDiscoveryEventType receivedEvent{SD::ServiceDiscoveryEventType::Invalid}; + SD::ServiceDescriptor received{}; + int callCount{0}; + serviceDiscovery.SetServiceDiscoveryHandler( + [&](SD::ServiceDiscoveryEventType eventType, const SD::ServiceDescriptor& descriptor) { + ++callCount; + receivedEvent = eventType; + received = descriptor; + }); + ASSERT_NE(capturedHandler, nullptr); + + // Case 1: a data publisher with media type and two labels. + SilKit_Label cLabels[2]; + cLabels[0].key = "kA"; + cLabels[0].value = "vA"; + cLabels[0].kind = SilKit_LabelKind_Mandatory; + cLabels[1].key = "kB"; + cLabels[1].value = "vB"; + cLabels[1].kind = SilKit_LabelKind_Optional; + + SilKit_Experimental_ServiceDescriptor cDescriptor; + SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, cDescriptor); + cDescriptor.participantName = "ParticipantA"; + cDescriptor.serviceName = "MyPublisher"; + cDescriptor.serviceKind = SilKit_Experimental_ServiceKind_DataPublisher; + cDescriptor.primaryIdentifier = "TopicA"; + cDescriptor.mediaType = "application/json"; + cDescriptor.labelList.numLabels = 2; + cDescriptor.labelList.labels = cLabels; + + capturedHandler(capturedContext, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, &cDescriptor); + + EXPECT_EQ(callCount, 1); + EXPECT_EQ(receivedEvent, SD::ServiceDiscoveryEventType::ServiceCreated); + EXPECT_EQ(received.participantName, "ParticipantA"); + EXPECT_EQ(received.serviceName, "MyPublisher"); + EXPECT_EQ(received.serviceKind, SD::ServiceKind::DataPublisher); + EXPECT_EQ(received.primaryIdentifier, "TopicA"); + EXPECT_EQ(received.mediaType, "application/json"); + ASSERT_EQ(received.labels.size(), 2u); + EXPECT_EQ(received.labels[0].key, "kA"); + EXPECT_EQ(received.labels[0].value, "vA"); + EXPECT_EQ(received.labels[0].kind, SilKit::Services::MatchingLabel::Kind::Mandatory); + EXPECT_EQ(received.labels[1].key, "kB"); + EXPECT_EQ(received.labels[1].kind, SilKit::Services::MatchingLabel::Kind::Optional); + + // Case 2: a bus controller removal without labels/media type. + SilKit_Experimental_ServiceDescriptor cBusDescriptor; + SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, cBusDescriptor); + cBusDescriptor.participantName = "ParticipantB"; + cBusDescriptor.serviceName = "Can1"; + cBusDescriptor.serviceKind = SilKit_Experimental_ServiceKind_CanController; + cBusDescriptor.primaryIdentifier = "CAN1"; + cBusDescriptor.mediaType = ""; + cBusDescriptor.labelList.numLabels = 0; + cBusDescriptor.labelList.labels = nullptr; + + capturedHandler(capturedContext, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, &cBusDescriptor); + + EXPECT_EQ(callCount, 2); + EXPECT_EQ(receivedEvent, SD::ServiceDiscoveryEventType::ServiceRemoved); + EXPECT_EQ(received.serviceKind, SD::ServiceKind::CanController); + EXPECT_EQ(received.primaryIdentifier, "CAN1"); + EXPECT_TRUE(received.mediaType.empty()); + EXPECT_TRUE(received.labels.empty()); +} + +TEST_F(Test_HourglassServiceDiscovery, to_string_maps_enums) +{ + EXPECT_EQ(SD::to_string(SD::ServiceKind::DataSubscriber), "DataSubscriber"); + EXPECT_EQ(SD::to_string(SD::ServiceKind::NetworkLink), "NetworkLink"); + EXPECT_EQ(SD::to_string(SD::ServiceDiscoveryEventType::ServiceCreated), "ServiceCreated"); + EXPECT_EQ(SD::to_string(SD::ServiceDiscoveryEventType::ServiceRemoved), "ServiceRemoved"); +} + +} // namespace diff --git a/SilKit/include/silkit/detail/impl/experimental/participant/ParticipantExtensions.ipp b/SilKit/include/silkit/detail/impl/experimental/participant/ParticipantExtensions.ipp index 60394257b..fb181aa17 100644 --- a/SilKit/include/silkit/detail/impl/experimental/participant/ParticipantExtensions.ipp +++ b/SilKit/include/silkit/detail/impl/experimental/participant/ParticipantExtensions.ipp @@ -8,9 +8,11 @@ #include "silkit/participant/IParticipant.hpp" #include "silkit/experimental/services/orchestration/ISystemController.hpp" #include "silkit/experimental/netsim/INetworkSimulator.hpp" +#include "silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp" #include "silkit/detail/impl/participant/Participant.hpp" #include "silkit/detail/impl/experimental/services/orchestration/SystemController.hpp" +#include "silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp" namespace SilKit { @@ -34,6 +36,14 @@ auto CreateNetworkSimulator(SilKit::IParticipant* cppIParticipant) return cppParticipant.ExperimentalCreateNetworkSimulator(); } +auto CreateServiceDiscovery(SilKit::IParticipant* cppIParticipant) + -> SilKit::Experimental::ServiceDiscovery::IServiceDiscovery* +{ + auto& cppParticipant = dynamic_cast(*cppIParticipant); + + return cppParticipant.ExperimentalCreateServiceDiscovery(); +} + } // namespace Participant } // namespace Experimental DETAIL_SILKIT_DETAIL_VN_NAMESPACE_CLOSE @@ -45,6 +55,7 @@ namespace Experimental { namespace Participant { using SilKit::DETAIL_SILKIT_DETAIL_NAMESPACE_NAME::Experimental::Participant::CreateSystemController; using SilKit::DETAIL_SILKIT_DETAIL_NAMESPACE_NAME::Experimental::Participant::CreateNetworkSimulator; +using SilKit::DETAIL_SILKIT_DETAIL_NAMESPACE_NAME::Experimental::Participant::CreateServiceDiscovery; } // namespace Participant } // namespace Experimental } // namespace SilKit diff --git a/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp b/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp new file mode 100644 index 000000000..58be201c9 --- /dev/null +++ b/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +#include "silkit/capi/SilKit.h" + +#include "silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp" + +#include "silkit/detail/impl/ThrowOnError.hpp" +#include "silkit/detail/macros.hpp" + + +namespace SilKit { +DETAIL_SILKIT_DETAIL_VN_NAMESPACE_BEGIN +namespace Impl { +namespace Experimental { +namespace ServiceDiscovery { + +class ServiceDiscovery : public SilKit::Experimental::ServiceDiscovery::IServiceDiscovery +{ +public: + inline explicit ServiceDiscovery(SilKit_Participant* participant); + + inline ~ServiceDiscovery() override = default; + + inline void SetServiceDiscoveryHandler( + SilKit::Experimental::ServiceDiscovery::ServiceDiscoveryHandler handler) override; + +public: + inline auto Get() const -> SilKit_Experimental_ServiceDiscovery*; + +private: + SilKit_Experimental_ServiceDiscovery* _serviceDiscovery{nullptr}; + std::unique_ptr _handler; +}; + +} // namespace ServiceDiscovery +} // namespace Experimental +} // namespace Impl +DETAIL_SILKIT_DETAIL_VN_NAMESPACE_CLOSE +} // namespace SilKit + + +// ================================================================================ +// Inline Implementations +// ================================================================================ + +namespace SilKit { +DETAIL_SILKIT_DETAIL_VN_NAMESPACE_BEGIN +namespace Impl { +namespace Experimental { +namespace ServiceDiscovery { + +ServiceDiscovery::ServiceDiscovery(SilKit_Participant* participant) +{ + const auto returnCode = SilKit_Experimental_ServiceDiscovery_Create(&_serviceDiscovery, participant); + ThrowOnError(returnCode); +} + +void ServiceDiscovery::SetServiceDiscoveryHandler( + SilKit::Experimental::ServiceDiscovery::ServiceDiscoveryHandler handler) +{ + auto ownedHandler = + std::make_unique(std::move(handler)); + + const auto cHandler = [](void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type eventType, + const SilKit_Experimental_ServiceDescriptor* cServiceDescriptor) { + namespace SD = SilKit::Experimental::ServiceDiscovery; + + SD::ServiceDescriptor serviceDescriptor{}; + serviceDescriptor.participantName = cServiceDescriptor->participantName; + serviceDescriptor.serviceName = cServiceDescriptor->serviceName; + serviceDescriptor.serviceKind = static_cast(cServiceDescriptor->serviceKind); + serviceDescriptor.primaryIdentifier = cServiceDescriptor->primaryIdentifier; + serviceDescriptor.mediaType = cServiceDescriptor->mediaType; + serviceDescriptor.labels.reserve(cServiceDescriptor->labelList.numLabels); + for (size_t i = 0; i < cServiceDescriptor->labelList.numLabels; ++i) + { + const auto& cLabel = cServiceDescriptor->labelList.labels[i]; + SilKit::Services::MatchingLabel label; + label.key = cLabel.key; + label.value = cLabel.value; + label.kind = static_cast(cLabel.kind); + serviceDescriptor.labels.emplace_back(std::move(label)); + } + + (*static_cast(context))( + static_cast(eventType), serviceDescriptor); + }; + + const auto returnCode = + SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(_serviceDiscovery, ownedHandler.get(), cHandler); + ThrowOnError(returnCode); + + _handler = std::move(ownedHandler); +} + +auto ServiceDiscovery::Get() const -> SilKit_Experimental_ServiceDiscovery* +{ + return _serviceDiscovery; +} + +} // namespace ServiceDiscovery +} // namespace Experimental +} // namespace Impl +DETAIL_SILKIT_DETAIL_VN_NAMESPACE_CLOSE +} // namespace SilKit diff --git a/SilKit/include/silkit/detail/impl/participant/Participant.hpp b/SilKit/include/silkit/detail/impl/participant/Participant.hpp index a3ebada3c..8bea2b6af 100644 --- a/SilKit/include/silkit/detail/impl/participant/Participant.hpp +++ b/SilKit/include/silkit/detail/impl/participant/Participant.hpp @@ -33,6 +33,7 @@ #include "silkit/detail/impl/services/rpc/RpcServer.hpp" #include "silkit/detail/impl/experimental/services/orchestration/SystemController.hpp" +#include "silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp" #include "silkit/detail/impl/netsim/NetworkSimulator.hpp" @@ -88,6 +89,9 @@ class Participant : public SilKit::IParticipant inline auto ExperimentalCreateSystemController() -> SilKit::Experimental::Services::Orchestration::ISystemController*; + inline auto ExperimentalCreateServiceDiscovery() + -> SilKit::Experimental::ServiceDiscovery::IServiceDiscovery*; + public: inline auto Get() const -> SilKit_Participant*; @@ -116,6 +120,8 @@ class Participant : public SilKit::IParticipant ControllerStorage _rpcClients; ControllerStorage _rpcServers; + ControllerStorage _serviceDiscoveries; + std::unique_ptr _lifecycleService; std::unique_ptr _systemMonitor; @@ -239,6 +245,11 @@ auto Participant::ExperimentalCreateNetworkSimulator() -> SilKit::Experimental:: return _networkSimulator.get(); } +auto Participant::ExperimentalCreateServiceDiscovery() -> SilKit::Experimental::ServiceDiscovery::IServiceDiscovery* +{ + return _serviceDiscoveries.Create(_participant); +} + auto Participant::Get() const -> SilKit_Participant* { return _participant; diff --git a/SilKit/include/silkit/experimental/participant/ParticipantExtensions.hpp b/SilKit/include/silkit/experimental/participant/ParticipantExtensions.hpp index f048eadab..fd695df32 100644 --- a/SilKit/include/silkit/experimental/participant/ParticipantExtensions.hpp +++ b/SilKit/include/silkit/experimental/participant/ParticipantExtensions.hpp @@ -7,6 +7,7 @@ #include "silkit/SilKitMacros.hpp" #include "silkit/participant/IParticipant.hpp" #include "silkit/experimental/services/orchestration/ISystemController.hpp" +#include "silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp" #include "silkit/detail/macros.hpp" @@ -28,6 +29,15 @@ DETAIL_SILKIT_CPP_API auto CreateSystemController(SilKit::IParticipant* particip DETAIL_SILKIT_CPP_API auto CreateNetworkSimulator(SilKit::IParticipant* participant) -> SilKit::Experimental::NetworkSimulation::INetworkSimulator*; +/*! \brief Return the experimental service-discovery observer at a given SIL Kit participant. +* +* \param participant The participant instance for which the observer is created. +* +* \throw SilKit::SilKitError The participant is invalid. +*/ +DETAIL_SILKIT_CPP_API auto CreateServiceDiscovery(SilKit::IParticipant* participant) + -> SilKit::Experimental::ServiceDiscovery::IServiceDiscovery*; + } // namespace Participant } // namespace Experimental diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp new file mode 100644 index 000000000..78b08e870 --- /dev/null +++ b/SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp" + +namespace SilKit { +namespace Experimental { +namespace ServiceDiscovery { + +/*! \brief Passively observes the user-facing services (bus controllers, publishers/subscribers, + * RPC clients/servers, network links) created by all participants in the simulation. + * + * \warning This interface is experimental and not part of the stable API and ABI of the SIL Kit. It + * may be removed or changed at any time without prior notice. + */ +class IServiceDiscovery +{ +public: + virtual ~IServiceDiscovery() = default; + + /*! \brief Register the handler that is called for every user-facing service in the simulation. + * + * Upon registration the handler is immediately invoked once for every service that is already + * known, each reported as \ref ServiceDiscoveryEventType::ServiceCreated. It is subsequently + * invoked for every user-facing service created or removed. Internal / infrastructure services + * are not reported. + * + * \param handler The handler to be called on service creation and removal. + */ + virtual void SetServiceDiscoveryHandler(ServiceDiscoveryHandler handler) = 0; +}; + +} // namespace ServiceDiscovery +} // namespace Experimental +} // namespace SilKit diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp new file mode 100644 index 000000000..c74d8f95a --- /dev/null +++ b/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +#include "silkit/capi/Experimental.h" +#include "silkit/services/datatypes.hpp" + +namespace SilKit { +namespace Experimental { +namespace ServiceDiscovery { + +//! \brief The kind of change reported for a discovered service. +enum class ServiceDiscoveryEventType : SilKit_Experimental_ServiceDiscoveryEvent_Type +{ + //! An invalid / unknown service discovery event. + Invalid = SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid, + //! A service has been created (or was already present on registration). + ServiceCreated = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, + //! A service has been removed. + ServiceRemoved = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, +}; + +//! \brief The kind of a discovered service. Only user-facing services are reported. +enum class ServiceKind : SilKit_Experimental_ServiceKind +{ + Undefined = SilKit_Experimental_ServiceKind_Undefined, + CanController = SilKit_Experimental_ServiceKind_CanController, + EthernetController = SilKit_Experimental_ServiceKind_EthernetController, + FlexrayController = SilKit_Experimental_ServiceKind_FlexrayController, + LinController = SilKit_Experimental_ServiceKind_LinController, + DataPublisher = SilKit_Experimental_ServiceKind_DataPublisher, + DataSubscriber = SilKit_Experimental_ServiceKind_DataSubscriber, + RpcClient = SilKit_Experimental_ServiceKind_RpcClient, + RpcServer = SilKit_Experimental_ServiceKind_RpcServer, + NetworkLink = SilKit_Experimental_ServiceKind_NetworkLink, +}; + +//! \brief Describes a single discovered service, passed to a \ref ServiceDiscoveryHandler. +struct ServiceDescriptor +{ + //! Name of the participant providing the service. + std::string participantName; + //! Name of the service (the controller / publisher / subscriber / client / server name). + std::string serviceName; + //! The kind of service. + ServiceKind serviceKind{ServiceKind::Undefined}; + //! The primary, user-facing identifier of the service: the network name for bus controllers + //! (e.g. "CAN1") and network links, the topic for pub/sub, and the function name for RPC. + std::string primaryIdentifier; + //! Media type for pub/sub and RPC services; empty string when not applicable. + std::string mediaType; + //! Decoded matching labels for pub/sub and RPC services; empty for bus controllers and links. + std::vector labels; +}; + +/*! \brief Handler invoked when a user-facing service is created or removed in the simulation. + * + * \param eventType Whether the service was created or removed. + * \param serviceDescriptor The affected service. + */ +using ServiceDiscoveryHandler = + std::function; + +} // namespace ServiceDiscovery +} // namespace Experimental +} // namespace SilKit diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp new file mode 100644 index 000000000..481f5d857 --- /dev/null +++ b/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +#include "silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp" + +namespace SilKit { +namespace Experimental { +namespace ServiceDiscovery { + +inline std::string to_string(ServiceKind serviceKind); +inline std::string to_string(ServiceDiscoveryEventType eventType); + +inline std::ostream& operator<<(std::ostream& out, ServiceKind serviceKind); +inline std::ostream& operator<<(std::ostream& out, ServiceDiscoveryEventType eventType); + +// ================================================================================ +// Inline Implementations +// ================================================================================ + +std::string to_string(ServiceKind serviceKind) +{ + switch (serviceKind) + { + case ServiceKind::Undefined: + return "Undefined"; + case ServiceKind::CanController: + return "CanController"; + case ServiceKind::EthernetController: + return "EthernetController"; + case ServiceKind::FlexrayController: + return "FlexrayController"; + case ServiceKind::LinController: + return "LinController"; + case ServiceKind::DataPublisher: + return "DataPublisher"; + case ServiceKind::DataSubscriber: + return "DataSubscriber"; + case ServiceKind::RpcClient: + return "RpcClient"; + case ServiceKind::RpcServer: + return "RpcServer"; + case ServiceKind::NetworkLink: + return "NetworkLink"; + } + + std::stringstream out; + out << "ServiceKind(" << static_cast(serviceKind) << ")"; + return out.str(); +} + +std::string to_string(ServiceDiscoveryEventType eventType) +{ + switch (eventType) + { + case ServiceDiscoveryEventType::Invalid: + return "Invalid"; + case ServiceDiscoveryEventType::ServiceCreated: + return "ServiceCreated"; + case ServiceDiscoveryEventType::ServiceRemoved: + return "ServiceRemoved"; + } + + std::stringstream out; + out << "ServiceDiscoveryEventType(" << static_cast(eventType) << ")"; + return out.str(); +} + +std::ostream& operator<<(std::ostream& out, ServiceKind serviceKind) +{ + return out << to_string(serviceKind); +} + +std::ostream& operator<<(std::ostream& out, ServiceDiscoveryEventType eventType) +{ + return out << to_string(eventType); +} + +} // namespace ServiceDiscovery +} // namespace Experimental +} // namespace SilKit From 18a5aa0b809574914b848cd4f680d892cfe03479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Thu, 16 Jul 2026 11:12:42 +0200 Subject: [PATCH 07/10] fixup! c++ hourglass on top of capi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- SilKit/source/capi/CapiExperimental.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SilKit/source/capi/CapiExperimental.cpp b/SilKit/source/capi/CapiExperimental.cpp index 125256a5d..235d4e095 100644 --- a/SilKit/source/capi/CapiExperimental.cpp +++ b/SilKit/source/capi/CapiExperimental.cpp @@ -231,8 +231,8 @@ struct PendingEmission // client/server are still reported on creation.) struct ObserverState { - SilKit_Experimental_ServiceDiscoveryHandler_t handler; - void* context; + SilKit_Experimental_ServiceDiscoveryHandler_t handler{}; + void* context{nullptr}; std::mutex mutex; std::map subscribers; @@ -242,11 +242,11 @@ struct ObserverState void Emit(ObserverState& state, SilKit_Experimental_ServiceDiscoveryEvent_Type type, const SilKit::Core::ServiceDescriptor& descriptor) { - SilKit_Experimental_ServiceDescriptor descriptor; + SilKit_Experimental_ServiceDescriptor reportedDescriptor{}; LabelStorage storage; - if (ClassifyAndFill(descriptor, descriptor, storage)) + if (ClassifyAndFill(descriptor, reportedDescriptor, storage)) { - state.handler(state.context, type, &descriptor); + state.handler(state.context, type, &reportedDescriptor); } } @@ -372,11 +372,11 @@ void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent // Publishers, RPC clients/servers, bus controllers, network links, and anything else: reported on // creation/removal exactly as before. - SilKit_Experimental_ServiceDescriptor descriptor; + SilKit_Experimental_ServiceDescriptor reportedDescriptor{}; LabelStorage storage; - if (ClassifyAndFill(descriptor, descriptor, storage)) + if (ClassifyAndFill(descriptor, reportedDescriptor, storage)) { - state.handler(state.context, ToC(type), &descriptor); + state.handler(state.context, ToC(type), &reportedDescriptor); } } From 0df294108f1edcfaf80efc5f51dce16a4b7263d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Tue, 21 Jul 2026 07:53:34 +0200 Subject: [PATCH 08/10] service created, inlining of netsim and linking subscribers to publishers into the struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- SilKit/IntegrationTests/CMakeLists.txt | 4 + .../Test_HourglassServiceDiscovery.cpp | 3 +- .../ITest_CapiServiceDiscovery.cpp | 164 +++++-- .../ITest_NetSimServiceDiscovery.cpp | 199 +++++++++ SilKit/include/silkit/capi/Experimental.h | 65 ++- .../serviceDiscovery/ServiceDiscovery.hpp | 6 + .../ServiceDiscoveryDatatypes.hpp | 25 +- .../serviceDiscovery/string_utils.hpp | 4 +- SilKit/source/capi/CapiExperimental.cpp | 418 +++++++++++++++--- .../source/capi/Test_CapiServiceDiscovery.cpp | 385 ++++++++++++++-- 10 files changed, 1095 insertions(+), 178 deletions(-) create mode 100644 SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp diff --git a/SilKit/IntegrationTests/CMakeLists.txt b/SilKit/IntegrationTests/CMakeLists.txt index fbfd344f6..ce8c48f1c 100644 --- a/SilKit/IntegrationTests/CMakeLists.txt +++ b/SilKit/IntegrationTests/CMakeLists.txt @@ -207,6 +207,10 @@ add_silkit_test_to_executable(SilKitIntegrationTests SOURCES ITest_NetSimFlexRay.cpp ) +add_silkit_test_to_executable(SilKitIntegrationTests + SOURCES ITest_NetSimServiceDiscovery.cpp +) + # ============================================================ # Integration Tests based on Demo Applications diff --git a/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp b/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp index e1e970d2b..2ef4075ae 100644 --- a/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp +++ b/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp @@ -153,9 +153,10 @@ TEST_F(Test_HourglassServiceDiscovery, service_discovery_handler_round_trip) TEST_F(Test_HourglassServiceDiscovery, to_string_maps_enums) { EXPECT_EQ(SD::to_string(SD::ServiceKind::DataSubscriber), "DataSubscriber"); - EXPECT_EQ(SD::to_string(SD::ServiceKind::NetworkLink), "NetworkLink"); + EXPECT_EQ(SD::to_string(SD::ServiceKind::RpcServer), "RpcServer"); EXPECT_EQ(SD::to_string(SD::ServiceDiscoveryEventType::ServiceCreated), "ServiceCreated"); EXPECT_EQ(SD::to_string(SD::ServiceDiscoveryEventType::ServiceRemoved), "ServiceRemoved"); + EXPECT_EQ(SD::to_string(SD::ServiceDiscoveryEventType::ServiceUpdated), "ServiceUpdated"); } } // namespace diff --git a/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp index aed5ad50d..1ebaa4789 100644 --- a/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp +++ b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp @@ -6,23 +6,11 @@ // (SilKit_Experimental_ServiceDiscovery_*), driven against real participants over an // in-process registry. // -// It validates the motivating use case: an observer correlating subscribers to the publisher -// they are connected to. The authoritative connection edge inside SIL Kit is the per-publisher -// transport UUID, carried by the internal DataSubscriberInternal service. The C API deliberately -// does not expose that internal service or the UUID. -// -// The intended design (follow-up) keeps the public struct as-is and instead does bookkeeping at -// the C API boundary over the internal DataPublisher/DataSubscriberInternal announcements, -// synthesizing a user-facing DataSubscriber ServiceCreated/ServiceRemoved event *if and only if* -// a valid internal match exists (i.e. the subscriber is actually connected to a publisher). -// -// Therefore: -// - The enabled tests below cover the matched happy-path observable today AND after the -// follow-up (the gated events fire in a matched scenario either way). -// - The DISABLED_ tests are the validators for the follow-up: they assert the *gating* -// (a subscriber with no matching publisher must produce no event) and the *removal -// synthesis* (destroying the publisher disconnects the subscriber). They fail today and are -// the acceptance criteria for the follow-up. Run with --gtest_also_run_disabled_tests. +// Design: subscribers and RPC servers are reported immediately on ServiceCreated (no connection +// gating). When a DataSubscriberInternal confirms a pub/sub match, the parent DataSubscriber +// receives a Service_Updated event carrying the updated numberOfConnections and the peer's +// identity. ServiceRemoved fires only when the service itself is destroyed, not when a connection +// drops. #include #include @@ -37,6 +25,7 @@ #include "silkit/capi/SilKit.h" #include "silkit/config/IParticipantConfiguration.hpp" #include "silkit/services/pubsub/all.hpp" +#include "silkit/services/rpc/all.hpp" #include "silkit/vendor/CreateSilKitRegistry.hpp" #include "gtest/gtest.h" @@ -47,6 +36,9 @@ using namespace std::chrono_literals; using SilKit::Services::PubSub::PubSubSpec; using SilKit::Services::PubSub::DataMessageEvent; using SilKit::Services::PubSub::IDataSubscriber; +using SilKit::Services::Rpc::RpcSpec; +using SilKit::Services::Rpc::RpcCallHandler; +using SilKit::Services::Rpc::RpcCallResultHandler; constexpr auto kWaitTimeout = 10s; @@ -68,6 +60,9 @@ struct ObservedEvent std::string primaryIdentifier; std::string mediaType; std::vector labels; + uint32_t numberOfConnections{0}; + std::string connectedParticipantName; + std::string connectedServiceName; }; // Shared state between the C callback thread and the test thread. @@ -97,6 +92,10 @@ void SilKitCALL OnServiceDiscovery(void* context, SilKit_Experimental_ServiceDis const auto& label = descriptor->labelList.labels[i]; event.labels.push_back({label.key, label.value, label.kind}); } + event.numberOfConnections = descriptor->numberOfConnections; + event.connectedParticipantName = + descriptor->connectedParticipantName ? descriptor->connectedParticipantName : ""; + event.connectedServiceName = descriptor->connectedServiceName ? descriptor->connectedServiceName : ""; { std::lock_guard lock{ctx->mutex}; @@ -182,6 +181,15 @@ class ITest_CapiServiceDiscovery : public testing::Test return it == _ctx.events.end() ? ObservedEvent{} : *it; } + // Returns a copy of the first observed event matching the predicate (caller must NOT hold lock). + template + auto FindEventWhere(Predicate pred) -> ObservedEvent + { + std::lock_guard lock{_ctx.mutex}; + const auto it = std::find_if(_ctx.events.begin(), _ctx.events.end(), pred); + return it == _ctx.events.end() ? ObservedEvent{} : *it; + } + std::unique_ptr _registry; std::string _registryUri; SilKit_Participant* _observer{nullptr}; @@ -277,17 +285,15 @@ TEST_F(ITest_CapiServiceDiscovery, both_subscribers_receive_published_data) EXPECT_TRUE(latch.Wait(2, kWaitTimeout)) << "both subscribers should receive the published sample"; } -// The gating discriminator. A subscriber whose topic has no publisher must NOT produce a -// DataSubscriber ServiceCreated event at the observer: the C API only reports a subscriber once it -// is connected to a publisher (a confirmed internal match). +// A subscriber with no matching publisher is reported immediately on creation with +// numberOfConnections == 0. The sentinel ensures in-order delivery (see below). // -// The barrier is deterministic (no wall-clock wait): the unmatched subscriber and a sentinel -// publisher are created, in that order, on the SAME participant. All of a participant's service -// announcements reach the observer over a single in-order connection, so the subscriber's -// announcement is delivered no later than the sentinel's. Once the observer has seen the sentinel -// publisher, it has necessarily already processed the subscriber's announcement -- so if the -// (removed) premature-report behavior were present, the event would already be recorded. -TEST_F(ITest_CapiServiceDiscovery, no_subscriber_event_without_matching_publisher) +// The barrier is deterministic: the unmatched subscriber and a sentinel publisher are created, in +// that order, on the SAME participant. All announcements from one participant reach the observer +// over a single in-order connection, so the subscriber's announcement is delivered no later than +// the sentinel's. Once the observer has seen the sentinel publisher, it has necessarily already +// processed the subscriber's announcement. +TEST_F(ITest_CapiServiceDiscovery, subscriber_visible_immediately_without_matching_publisher) { const std::string lonelyTopic{"LonelyTopic"}; const std::string sentinelTopic{"SentinelTopic"}; @@ -307,16 +313,21 @@ TEST_F(ITest_CapiServiceDiscovery, no_subscriber_event_without_matching_publishe >= 1; })) << "observer never saw the sentinel publisher"; + // Subscriber was announced before the sentinel; the observer must have seen it by now. EXPECT_EQ(Count(SilKit_Experimental_ServiceKind_DataSubscriber, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, lonelyTopic), - 0u) - << "an unmatched subscriber must not be reported as connected"; + 1u) + << "a subscriber must be reported immediately on creation, even without a matching publisher"; + + // And the connection count must be zero (no publisher matched). + const auto ev = FindEvent(SilKit_Experimental_ServiceKind_DataSubscriber, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, lonelyTopic); + EXPECT_EQ(ev.numberOfConnections, 0u); } -// The removal-synthesis half. Start matched (observer sees the subscriber connected), then destroy -// the publisher; the observer must receive a synthesized DataSubscriber ServiceRemoved -// (disconnected) event once the subscriber's last connection drops. -TEST_F(ITest_CapiServiceDiscovery, subscriber_event_removed_when_publisher_leaves) +// When the publisher leaves, the observer must receive a Service_Updated event on the subscriber +// with numberOfConnections == 0. ServiceRemoved does NOT fire (the subscriber is still alive). +TEST_F(ITest_CapiServiceDiscovery, subscriber_updated_when_publisher_leaves) { const std::string topic{"T"}; const std::string mediaType{"M"}; @@ -327,19 +338,90 @@ TEST_F(ITest_CapiServiceDiscovery, subscriber_event_removed_when_publisher_leave publisher->CreateDataPublisher("PubCtrl", spec, 1); subscriber->CreateDataSubscriber("SubCtrl", spec, [](IDataSubscriber*, const DataMessageEvent&) {}); + // Wait for the connection to be established before destroying the publisher. ASSERT_TRUE(WaitFor([&] { - return CountUnlocked(SilKit_Experimental_ServiceKind_DataSubscriber, - SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic) - >= 1; - })) << "observer never saw the connected subscriber"; + return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated + && e.primaryIdentifier == topic && e.numberOfConnections >= 1; + }); + })) << "observer never saw the subscriber connected to the publisher (Service_Updated, connections>=1)"; publisher.reset(); ASSERT_TRUE(WaitFor([&] { - return CountUnlocked(SilKit_Experimental_ServiceKind_DataSubscriber, - SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, topic) - >= 1; - })) << "observer should see the subscriber disconnected when the publisher leaves"; + return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated + && e.primaryIdentifier == topic && e.numberOfConnections == 0; + }); + })) << "observer should see Service_Updated(connections=0) when the publisher leaves"; +} + +// When a publisher matches a subscriber, the Service_Updated event carried on the subscriber must +// name the publisher: connectedParticipantName == publisher's participant name and +// connectedServiceName == publisher's controller name. This lets the observer build a confirmed +// connection graph from service discovery data alone. +TEST_F(ITest_CapiServiceDiscovery, pubsub_service_updated_carries_publisher_peer_identity) +{ + const std::string topic{"Sensor"}; + const std::string mediaType{"application/octet-stream"}; + + auto publisher = CreateParticipant("PublisherParticipant"); + auto subscriber = CreateParticipant("SubscriberParticipant"); + publisher->CreateDataPublisher("PublisherController", PubSubSpec{topic, mediaType}, 0); + subscriber->CreateDataSubscriber("SubscriberController", PubSubSpec{topic, mediaType}, + [](IDataSubscriber*, const DataMessageEvent&) {}); + + ASSERT_TRUE(WaitFor([&] { + return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated + && e.primaryIdentifier == topic && e.numberOfConnections >= 1; + }); + })) << "Service_Updated for matched subscriber never arrived"; + + const auto ev = FindEventWhere([&](const ObservedEvent& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated + && e.primaryIdentifier == topic && e.numberOfConnections >= 1; + }); + EXPECT_EQ(ev.connectedParticipantName, "PublisherParticipant") + << "subscriber's Service_Updated must identify the publisher's participant"; + EXPECT_EQ(ev.connectedServiceName, "PublisherController") + << "subscriber's Service_Updated must identify the publisher's controller name"; +} + +// The RPC server's Service_Updated must carry the matching client's participant name and +// controller name, letting the observer resolve concrete RPC call edges. +TEST_F(ITest_CapiServiceDiscovery, rpc_service_updated_carries_client_peer_identity) +{ + const std::string functionName{"RemoteProc"}; + const std::string mediaType{"application/octet-stream"}; + RpcSpec spec{functionName, mediaType}; + + auto server = CreateParticipant("ServerParticipant"); + auto client = CreateParticipant("ClientParticipant"); + server->CreateRpcServer("ServerController", spec, RpcCallHandler{[](auto*, auto) {}}); + client->CreateRpcClient("ClientController", spec, RpcCallResultHandler{[](auto*, auto) {}}); + + ASSERT_TRUE(WaitFor([&] { + return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_RpcServer + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated + && e.primaryIdentifier == functionName && e.numberOfConnections >= 1; + }); + })) << "Service_Updated for matched RPC server never arrived"; + + const auto ev = FindEventWhere([&](const ObservedEvent& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_RpcServer + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated + && e.primaryIdentifier == functionName && e.numberOfConnections >= 1; + }); + EXPECT_EQ(ev.connectedParticipantName, "ClientParticipant") + << "server's Service_Updated must identify the RPC client's participant"; + EXPECT_EQ(ev.connectedServiceName, "ClientController") + << "server's Service_Updated must identify the RPC client's controller name"; } } // namespace diff --git a/SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp b/SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp new file mode 100644 index 000000000..503a5e12f --- /dev/null +++ b/SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +// Integration test: verify that the service-discovery C API correctly reports bus controllers as +// simulated (isSimulated=true) when a network simulator is present, without embedding this +// plumbing into the CAN/Ethernet/LIN/FlexRay routing tests. +// +// The test sets up the minimal scenario: one network simulator participant that takes over one CAN +// network ("CAN1"), one regular participant with a CAN controller on that network, and one +// autonomous service-discovery observer. After the simulation, the observer must have received at +// least one event for a CAN controller with isSimulated=true and the correct +// simulatingParticipantName. + +#include +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" + +#include "ITestFixture.hpp" + +#include "silkit/SilKit.hpp" +#include "silkit/capi/SilKit.h" +#include "silkit/experimental/netsim/all.hpp" +#include "silkit/services/can/all.hpp" + +namespace { + +using namespace std::chrono_literals; +using namespace SilKit::Tests; +using namespace SilKit::Experimental::NetworkSimulation; +using namespace SilKit::Experimental::NetworkSimulation::Can; + +// --------------------------------------------------------------------------- +// Minimal simulated CAN network — no message routing needed for this test. +// --------------------------------------------------------------------------- + +class MinimalSimulatedCanController : public ISimulatedCanController +{ +public: + void OnSetControllerMode(const CanControllerMode&) override {} + void OnSetBaudrate(const CanConfigureBaudrate&) override {} + void OnFrameRequest(const CanFrameRequest&) override {} +}; + +class MinimalSimulatedCanNetwork : public ISimulatedNetwork +{ + std::unique_ptr _producer; + std::vector> _controllers; + +public: + void SetEventProducer(std::unique_ptr producer) override + { + _producer = std::move(producer); + } + + auto ProvideSimulatedController(ControllerDescriptor) -> ISimulatedController* override + { + _controllers.push_back(std::make_unique()); + return _controllers.back().get(); + } + + void SimulatedControllerRemoved(ControllerDescriptor) override {} +}; + +// --------------------------------------------------------------------------- +// Observer state — accumulated service-discovery events from the C API. +// --------------------------------------------------------------------------- + +struct DiscoveryEvent +{ + SilKit_Experimental_ServiceKind serviceKind{}; + SilKit_Experimental_ServiceDiscoveryEvent_Type eventType{}; + std::string primaryIdentifier; // networkName for bus controllers + bool isSimulated{false}; + std::string simulatingParticipantName; +}; + +struct DiscoveryCtx +{ + std::mutex mutex; + std::condition_variable cv; + std::vector events; +}; + +void SilKitCALL OnNetSimDiscovery(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type eventType, + const SilKit_Experimental_ServiceDescriptor* d) +{ + auto* ctx = static_cast(context); + + DiscoveryEvent ev{}; + ev.serviceKind = d->serviceKind; + ev.eventType = eventType; + ev.primaryIdentifier = d->primaryIdentifier ? d->primaryIdentifier : ""; + ev.isSimulated = d->isSimulated != SilKit_False; + ev.simulatingParticipantName = d->simulatingParticipantName ? d->simulatingParticipantName : ""; + + { + std::lock_guard lk{ctx->mutex}; + ctx->events.push_back(std::move(ev)); + } + ctx->cv.notify_all(); +} + +// --------------------------------------------------------------------------- +// Test fixture +// --------------------------------------------------------------------------- + +class ITest_NetSimServiceDiscovery : public ITest_SimTestHarness +{ +}; + +// A network simulator takes over the "CAN1" network. A regular participant creates a CAN +// controller on that network. The service-discovery observer must see the CAN controller event +// with isSimulated=true and simulatingParticipantName equal to the network simulator's +// participant name — demonstrating that the new API can identify a network simulator without +// any dedicated "NetworkLink" kind or separate cross-referencing by the caller. +TEST_F(ITest_NetSimServiceDiscovery, can_controller_reported_as_simulated_when_netsim_present) +{ + const std::string netSimName = "NetworkSimulator"; + const std::string canPartName = "CanParticipant"; + const std::string networkName = "CAN1"; + + SetupFromParticipantList({netSimName, canPartName}); + + // Service-discovery observer: autonomous participant, NOT part of the sync group. + // Creates a service discovery handle and registers a handler before the simulation starts, + // so it sees all service announcements as participants join. + DiscoveryCtx ctx; + SilKit_ParticipantConfiguration* cfg{nullptr}; + ASSERT_EQ(SilKit_ParticipantConfiguration_FromString(&cfg, ""), SilKit_ReturnCode_SUCCESS); + SilKit_Participant* observer{nullptr}; + ASSERT_EQ(SilKit_Participant_Create(&observer, cfg, "Observer", _registryUri.c_str()), + SilKit_ReturnCode_SUCCESS); + SilKit_ParticipantConfiguration_Destroy(cfg); + + SilKit_Experimental_ServiceDiscovery* sd{nullptr}; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&sd, observer), SilKit_ReturnCode_SUCCESS); + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(sd, &ctx, &OnNetSimDiscovery), + SilKit_ReturnCode_SUCCESS); + + // Network simulator participant: claims the CAN network and stops the simulation after 5 ms. + { + auto* p = _simTestHarness->GetParticipant(netSimName); + auto* lc = p->GetOrCreateLifecycleService(); + auto* ts = p->GetOrCreateTimeSyncService(); + auto* netsim = p->GetOrCreateNetworkSimulator(); + + netsim->SimulateNetwork(networkName, SimulatedNetworkType::CAN, + std::make_unique()); + netsim->Start(); + + ts->SetSimulationStepHandler( + [lc](auto now, auto) { + if (now >= 5ms) + lc->Stop("done"); + }, 1ms); + } + + // Regular participant: creates one CAN controller on the simulated network. + { + auto* p = _simTestHarness->GetParticipant(canPartName); + auto* ts = p->GetOrCreateTimeSyncService(); + p->Participant()->CreateCanController("CanCtrl", networkName); + ts->SetSimulationStepHandler([](auto, auto) {}, 1ms); + } + + auto ok = _simTestHarness->Run(10s); + ASSERT_TRUE(ok) << "simulation should complete without timeout"; + + // The isSimulated=true events for the CAN controller fire during the simulation (when the + // Link service from the network simulator arrives). They are accumulated in ctx.events by + // the observer's IO thread. Allow a brief settle window after Run() returns for any + // in-flight IO delivery. + { + std::unique_lock lk{ctx.mutex}; + const bool found = ctx.cv.wait_for(lk, 5s, [&] { + return std::any_of(ctx.events.begin(), ctx.events.end(), [](const DiscoveryEvent& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_CanController && e.isSimulated; + }); + }); + ASSERT_TRUE(found) << "no CAN controller event with isSimulated=true was observed"; + + const auto it = std::find_if(ctx.events.begin(), ctx.events.end(), [](const DiscoveryEvent& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_CanController && e.isSimulated; + }); + EXPECT_EQ(it->simulatingParticipantName, netSimName) + << "simulatingParticipantName must be the network simulator's participant name"; + } + + SilKit_Participant_Destroy(observer); +} + +} // namespace diff --git a/SilKit/include/silkit/capi/Experimental.h b/SilKit/include/silkit/capi/Experimental.h index 32ca191d7..1bf8170ff 100644 --- a/SilKit/include/silkit/capi/Experimental.h +++ b/SilKit/include/silkit/capi/Experimental.h @@ -17,10 +17,10 @@ SILKIT_BEGIN_DECLS // Experimental service discovery // // Allows a participant to passively observe the user-facing services (bus -// controllers, publishers/subscribers, RPC clients/servers, network links) -// created by all other participants in the simulation. This builds on the -// internal service discovery used throughout the SIL Kit and requires no -// configuration of the observed participants. +// controllers, publishers/subscribers, RPC clients/servers) created by all +// other participants in the simulation. This builds on the internal service +// discovery used throughout the SIL Kit and requires no configuration of the +// observed participants. // // \warning The functions and types declared in this header are not part of the // stable API and ABI of the SIL Kit. They may be removed or changed at @@ -37,6 +37,9 @@ typedef uint32_t SilKit_Experimental_ServiceDiscoveryEvent_Type; /*! \brief A service has been removed. */ #define SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved \ ((SilKit_Experimental_ServiceDiscoveryEvent_Type)2) +/*! \brief A property of an existing service has changed (e.g. connection count, simulation status). */ +#define SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated \ + ((SilKit_Experimental_ServiceDiscoveryEvent_Type)3) /*! \brief The kind of a discovered service. Only user-facing services are reported. */ typedef uint32_t SilKit_Experimental_ServiceKind; @@ -49,12 +52,23 @@ typedef uint32_t SilKit_Experimental_ServiceKind; #define SilKit_Experimental_ServiceKind_DataSubscriber ((SilKit_Experimental_ServiceKind)6) #define SilKit_Experimental_ServiceKind_RpcClient ((SilKit_Experimental_ServiceKind)7) #define SilKit_Experimental_ServiceKind_RpcServer ((SilKit_Experimental_ServiceKind)8) -#define SilKit_Experimental_ServiceKind_NetworkLink ((SilKit_Experimental_ServiceKind)9) /*! \brief Describes a single discovered service, passed by value to a service discovery handler. * - * \warning All pointer members (the strings and the label list) are borrowed and only valid for the duration of the - * handler invocation. Copy any data that must outlive the call. + * All pointer members are borrowed and only valid for the duration of the handler invocation. Copy + * any data that must outlive the call. + * + * Fields \p connectedParticipantName and \p connectedServiceName are populated only in + * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated events that represent a + * connection change (pub/sub match or RPC call). They identify the single peer whose connection + * was added or removed, while \p numberOfConnections gives the new running total. For all other + * event types and all other service kinds these fields are empty strings. + * + * Fields \p isSimulated and \p simulatingParticipantName are set on bus controllers (CAN, Ethernet, + * FlexRay, LIN) when a network simulator claims ownership of the controller's network. Both a + * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated with \p isSimulated = true + * (simulator already active) and a subsequent + * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated are possible. */ typedef struct { @@ -65,23 +79,35 @@ typedef struct const char* serviceName; //! The kind of service. SilKit_Experimental_ServiceKind serviceKind; - //! The primary, user-facing identifier of the service: the network name for bus controllers (e.g. "CAN1") and - //! network links, the topic for pub/sub, and the function name for RPC. Suitable as a display / join key for + //! The primary, user-facing identifier of the service: the network name for bus controllers, + //! the topic for pub/sub, and the function name for RPC. Suitable as a display / join key for //! visualization and tooling. const char* primaryIdentifier; //! Media type for pub/sub and RPC services; empty string when not applicable. const char* mediaType; - //! Decoded matching labels for pub/sub and RPC services; empty for bus controllers and links. + //! Decoded matching labels for pub/sub and RPC services; empty for bus controllers. SilKit_LabelList labelList; + //! The simulation name this service belongs to. Empty string when not available. + const char* simulationName; + //! Name of the peer participant; populated only in ServiceUpdated connection events. + const char* connectedParticipantName; + //! Name of the peer service; populated only in ServiceUpdated connection events. + const char* connectedServiceName; + //! Name of the participant simulating this controller's network; empty when not simulated. + const char* simulatingParticipantName; + //! Number of active matched connections (pub/sub matches or RPC call pairs); 0 for bus controllers. + uint32_t numberOfConnections; + //! True when a network simulator owns this bus controller's network. + SilKit_Bool isSimulated; } SilKit_Experimental_ServiceDescriptor; -/*! \brief Handler invoked when a user-facing service is created or removed in the simulation. +/*! \brief Handler invoked when a user-facing service is created, updated, or removed in the simulation. * - * The \p serviceDescriptor and all of its pointer members are only valid for the duration of the handler invocation. - * Copy the data if it must outlive the call. + * The \p serviceDescriptor and all of its pointer members are only valid for the duration of the + * handler invocation. Copy the data if it must outlive the call. * * \param context The user context pointer passed to \ref SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler. - * \param eventType Whether the service was created or removed. + * \param eventType Whether the service was created, updated, or removed. * \param serviceDescriptor The affected service. */ typedef void(SilKitFPTR* SilKit_Experimental_ServiceDiscoveryHandler_t)( @@ -106,16 +132,19 @@ typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDiscovery_Creat /*! \brief Register a handler that is called for every user-facing service in the simulation. * - * Upon registration the handler is immediately invoked once for every service that is already known, each reported as - * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated. It is subsequently invoked for every user-facing - * service created or removed. Internal/infrastructure services are not reported. + * Upon registration the handler is immediately invoked once for every service that is already known, + * each reported as \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated. It is + * subsequently invoked for every user-facing service created, updated, or removed. + * Infrastructure/internal services are not reported. Network link events are not reported directly; + * instead, affected bus controllers receive a + * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated with \p isSimulated set. * * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without * prior notice. * * \param serviceDiscovery The observer obtained via \ref SilKit_Experimental_ServiceDiscovery_Create. * \param context The user context pointer made available to the handler. - * \param handler The handler to be called on service creation and removal. + * \param handler The handler to be called on service creation, update, and removal. */ SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler( SilKit_Experimental_ServiceDiscovery* serviceDiscovery, void* context, diff --git a/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp b/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp index 58be201c9..7e06f075e 100644 --- a/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp +++ b/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp @@ -87,6 +87,12 @@ void ServiceDiscovery::SetServiceDiscoveryHandler( label.kind = static_cast(cLabel.kind); serviceDescriptor.labels.emplace_back(std::move(label)); } + serviceDescriptor.simulationName = cServiceDescriptor->simulationName ? cServiceDescriptor->simulationName : ""; + serviceDescriptor.connectedParticipantName = cServiceDescriptor->connectedParticipantName ? cServiceDescriptor->connectedParticipantName : ""; + serviceDescriptor.connectedServiceName = cServiceDescriptor->connectedServiceName ? cServiceDescriptor->connectedServiceName : ""; + serviceDescriptor.simulatingParticipantName = cServiceDescriptor->simulatingParticipantName ? cServiceDescriptor->simulatingParticipantName : ""; + serviceDescriptor.numberOfConnections = cServiceDescriptor->numberOfConnections; + serviceDescriptor.isSimulated = cServiceDescriptor->isSimulated != SilKit_False; (*static_cast(context))( static_cast(eventType), serviceDescriptor); diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp index c74d8f95a..d7d93bd30 100644 --- a/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp +++ b/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp @@ -24,6 +24,8 @@ enum class ServiceDiscoveryEventType : SilKit_Experimental_ServiceDiscoveryEvent ServiceCreated = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, //! A service has been removed. ServiceRemoved = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, + //! A property of an existing service has changed (e.g. connection count, simulation status). + ServiceUpdated = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated, }; //! \brief The kind of a discovered service. Only user-facing services are reported. @@ -38,7 +40,6 @@ enum class ServiceKind : SilKit_Experimental_ServiceKind DataSubscriber = SilKit_Experimental_ServiceKind_DataSubscriber, RpcClient = SilKit_Experimental_ServiceKind_RpcClient, RpcServer = SilKit_Experimental_ServiceKind_RpcServer, - NetworkLink = SilKit_Experimental_ServiceKind_NetworkLink, }; //! \brief Describes a single discovered service, passed to a \ref ServiceDiscoveryHandler. @@ -50,18 +51,30 @@ struct ServiceDescriptor std::string serviceName; //! The kind of service. ServiceKind serviceKind{ServiceKind::Undefined}; - //! The primary, user-facing identifier of the service: the network name for bus controllers - //! (e.g. "CAN1") and network links, the topic for pub/sub, and the function name for RPC. + //! The primary, user-facing identifier of the service: the network name for bus controllers, + //! the topic for pub/sub, and the function name for RPC. std::string primaryIdentifier; //! Media type for pub/sub and RPC services; empty string when not applicable. std::string mediaType; - //! Decoded matching labels for pub/sub and RPC services; empty for bus controllers and links. + //! Decoded matching labels for pub/sub and RPC services; empty for bus controllers. std::vector labels; + //! The simulation name this service belongs to. Empty string when not available. + std::string simulationName; + //! Name of the peer participant; populated only in ServiceUpdated connection events. + std::string connectedParticipantName; + //! Name of the peer service; populated only in ServiceUpdated connection events. + std::string connectedServiceName; + //! Name of the participant simulating this controller's network; empty when not simulated. + std::string simulatingParticipantName; + //! Number of active matched connections (pub/sub or RPC); 0 for bus controllers. + uint32_t numberOfConnections{0}; + //! True when a network simulator owns this bus controller's network. + bool isSimulated{false}; }; -/*! \brief Handler invoked when a user-facing service is created or removed in the simulation. +/*! \brief Handler invoked when a user-facing service is created, updated, or removed in the simulation. * - * \param eventType Whether the service was created or removed. + * \param eventType Whether the service was created, updated, or removed. * \param serviceDescriptor The affected service. */ using ServiceDiscoveryHandler = diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp index 481f5d857..5915b0f2e 100644 --- a/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp +++ b/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp @@ -46,8 +46,6 @@ std::string to_string(ServiceKind serviceKind) return "RpcClient"; case ServiceKind::RpcServer: return "RpcServer"; - case ServiceKind::NetworkLink: - return "NetworkLink"; } std::stringstream out; @@ -65,6 +63,8 @@ std::string to_string(ServiceDiscoveryEventType eventType) return "ServiceCreated"; case ServiceDiscoveryEventType::ServiceRemoved: return "ServiceRemoved"; + case ServiceDiscoveryEventType::ServiceUpdated: + return "ServiceUpdated"; } std::stringstream out; diff --git a/SilKit/source/capi/CapiExperimental.cpp b/SilKit/source/capi/CapiExperimental.cpp index 235d4e095..e0f95ed39 100644 --- a/SilKit/source/capi/CapiExperimental.cpp +++ b/SilKit/source/capi/CapiExperimental.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -61,7 +62,10 @@ struct LabelStorage }; // Maps the internal ServiceDescriptor to the public struct. Returns false if the service is not user-facing -// (infrastructure / internal services), in which case the handler must not be invoked. +// (infrastructure / internal services / network links), in which case the handler must not be invoked. +// Connection-specific and simulation-specific fields (numberOfConnections, connectedParticipantName, +// connectedServiceName, isSimulated, simulatingParticipantName) are initialised to safe empty values +// here; callers set them from PendingEmission after this function returns. auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, SilKit_Experimental_ServiceDescriptor& out, LabelStorage& storage) -> bool { @@ -77,12 +81,16 @@ auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, out.serviceName = serviceDescriptor.GetServiceName().c_str(); out.primaryIdentifier = serviceDescriptor.GetNetworkName().c_str(); out.mediaType = ""; + out.simulationName = serviceDescriptor.GetSimulationName().c_str(); + out.connectedParticipantName = ""; + out.connectedServiceName = ""; + out.simulatingParticipantName = ""; + // numberOfConnections = 0, isSimulated = SilKit_False (from memset above) - // Network links carry no controller type and are reported as-is. + // Network links are handled internally (fan-out to bus controllers) and are not exposed directly. if (serviceDescriptor.GetServiceType() == SilKit::Core::ServiceType::Link) { - out.serviceKind = SilKit_Experimental_ServiceKind_NetworkLink; - return true; + return false; } std::string controllerType; @@ -200,116 +208,370 @@ auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, // Identifies a service across the simulation: (participant name, service id). using ServiceKey = std::pair; +// Identifies a network: (network name, network type). Used to correlate bus controllers with links. +using NetworkKey = std::pair; -struct SubscriberEntry +// Peer identity for pub/sub and RPC connection resolution. +struct PeerInfo +{ + std::string participantName; + std::string serviceName; +}; + +// Tracks a user-facing service (DataSubscriber or RpcServer) for connection counting. +struct TrackedEntry { SilKit::Core::ServiceDescriptor descriptor; bool haveDescriptor{false}; std::size_t connections{0}; - bool announced{false}; }; // A synthesized event, queued while the state lock is held and dispatched after it is released. +// All std::string members hold backing storage for the pointer fields set in the emitted struct. struct PendingEmission { - SilKit_Experimental_ServiceDiscoveryEvent_Type type; + SilKit_Experimental_ServiceDiscoveryEvent_Type type{SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid}; SilKit::Core::ServiceDescriptor descriptor; + uint32_t numberOfConnections{0}; + std::string connectedParticipantName; + std::string connectedServiceName; + bool isSimulated{false}; + std::string simulatingParticipantName; }; // Per-observer bookkeeping. Lives as long as the participant's service discovery, because it is // captured by the registered handler (which is never unregistered). -// -// Semantics: a user-facing DataSubscriber is reported to the observer only while it has at least one -// confirmed connection to a publisher. SIL Kit creates exactly one internal DataSubscriberInternal -// service per matched publisher - its existence IS the confirmed match - carrying the parent -// DataSubscriber's service id. We ref-count these connections and synthesize a ServiceCreated when -// the first connection appears and a ServiceRemoved when the last one disappears. Internal services -// and the transport UUID are never exposed. All other kinds (publishers, RPC, bus, links) are -// reported on creation, unchanged. -// -// (RPC has an analogous RpcServerInternal edge but is intentionally out of scope here: RPC -// client/server are still reported on creation.) struct ObserverState { SilKit_Experimental_ServiceDiscoveryHandler_t handler{}; void* context{nullptr}; std::mutex mutex; - std::map subscribers; - std::map connectionParent; // internal-subscriber id -> parent subscriber id + + // DataSubscribers and RpcServers tracked for connection counting and peer resolution. + std::map tracked; + // internal-connection key -> parent (subscriber/server) key + std::map connectionParent; + + // DataPublisher UUID (= networkName of DataPublisher) -> peer info, for resolving pub/sub connections. + std::map publishersByUuid; + // RpcClient UUID (= networkName of RpcClient) -> peer info, for resolving RPC connections. + std::map rpcClientsByUuid; + + // Bus controllers by ServiceKey -> stored descriptor, for link-event fan-out. + std::map busControllers; + // NetworkKey -> set of bus controller ServiceKeys (reverse lookup for link events). + std::map> controllersByNetwork; + + // Active network simulators: NetworkKey -> simulator's participant name. + std::map activeLinks; }; -void Emit(ObserverState& state, SilKit_Experimental_ServiceDiscoveryEvent_Type type, - const SilKit::Core::ServiceDescriptor& descriptor) +void EmitPending(ObserverState& state, const PendingEmission& e) { - SilKit_Experimental_ServiceDescriptor reportedDescriptor{}; + SilKit_Experimental_ServiceDescriptor out{}; LabelStorage storage; - if (ClassifyAndFill(descriptor, reportedDescriptor, storage)) + if (!ClassifyAndFill(e.descriptor, out, storage)) { - state.handler(state.context, type, &reportedDescriptor); + return; } + out.numberOfConnections = e.numberOfConnections; + out.connectedParticipantName = e.connectedParticipantName.c_str(); + out.connectedServiceName = e.connectedServiceName.c_str(); + out.isSimulated = e.isSimulated ? SilKit_True : SilKit_False; + out.simulatingParticipantName = e.simulatingParticipantName.c_str(); + state.handler(state.context, e.type, &out); } -// Handles a single internal discovery event. Subscriber-related events drive the connection -// bookkeeping and may synthesize user-facing DataSubscriber events; every other kind is forwarded -// as-is. Emissions are dispatched outside the lock so user code never runs while we hold it. +// Handles a single internal discovery event, translating it into zero or more public emissions. +// Emissions are always dispatched outside the state lock so user code never runs while we hold it. void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent::Type type, const SilKit::Core::ServiceDescriptor& descriptor) { using EventType = Discovery::ServiceDiscoveryEvent::Type; + // ---- Network links: track and fan-out isSimulated updates to affected bus controllers ---- + if (descriptor.GetServiceType() == SilKit::Core::ServiceType::Link) + { + const NetworkKey networkKey{descriptor.GetNetworkName(), descriptor.GetNetworkType()}; + std::vector emissions; + { + std::lock_guard lock{state.mutex}; + if (type == EventType::ServiceCreated) + { + state.activeLinks[networkKey] = descriptor.GetParticipantName(); + const auto cit = state.controllersByNetwork.find(networkKey); + if (cit != state.controllersByNetwork.end()) + { + for (const auto& controllerKey : cit->second) + { + PendingEmission e; + e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; + e.descriptor = state.busControllers.at(controllerKey); + e.isSimulated = true; + e.simulatingParticipantName = descriptor.GetParticipantName(); + emissions.push_back(std::move(e)); + } + } + } + else if (type == EventType::ServiceRemoved) + { + state.activeLinks.erase(networkKey); + const auto cit = state.controllersByNetwork.find(networkKey); + if (cit != state.controllersByNetwork.end()) + { + for (const auto& controllerKey : cit->second) + { + PendingEmission e; + e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; + e.descriptor = state.busControllers.at(controllerKey); + e.isSimulated = false; + emissions.push_back(std::move(e)); + } + } + } + } + for (const auto& e : emissions) + { + EmitPending(state, e); + } + return; + } + std::string controllerType; descriptor.GetSupplementalDataItem(Discovery::controllerType, controllerType); + // ---- Bus controllers: track for link fan-out; emit immediately with current isSimulated state ---- + if (controllerType == Discovery::controllerTypeCan || controllerType == Discovery::controllerTypeEthernet + || controllerType == Discovery::controllerTypeFlexray || controllerType == Discovery::controllerTypeLin) + { + const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; + const NetworkKey networkKey{descriptor.GetNetworkName(), descriptor.GetNetworkType()}; + PendingEmission emission; + emission.descriptor = descriptor; + { + std::lock_guard lock{state.mutex}; + if (type == EventType::ServiceCreated) + { + state.busControllers[key] = descriptor; + state.controllersByNetwork[networkKey].insert(key); + emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; + const auto linkIt = state.activeLinks.find(networkKey); + if (linkIt != state.activeLinks.end()) + { + emission.isSimulated = true; + emission.simulatingParticipantName = linkIt->second; + } + } + else if (type == EventType::ServiceRemoved) + { + state.busControllers.erase(key); + auto& ctrlSet = state.controllersByNetwork[networkKey]; + ctrlSet.erase(key); + if (ctrlSet.empty()) + { + state.controllersByNetwork.erase(networkKey); + } + emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; + } + } + EmitPending(state, emission); + return; + } + + // ---- DataPublisher: track UUID for connection peer resolution; emit immediately ---- + if (controllerType == Discovery::controllerTypeDataPublisher) + { + { + std::lock_guard lock{state.mutex}; + if (type == EventType::ServiceCreated) + { + state.publishersByUuid[descriptor.GetNetworkName()] = {descriptor.GetParticipantName(), + descriptor.GetServiceName()}; + } + else if (type == EventType::ServiceRemoved) + { + state.publishersByUuid.erase(descriptor.GetNetworkName()); + } + } + PendingEmission e; + e.type = ToC(type); + e.descriptor = descriptor; + EmitPending(state, e); + return; + } + + // ---- DataSubscriber: emit immediately; tracked for connection counting via DataSubscriberInternal ---- if (controllerType == Discovery::controllerTypeDataSubscriber) { const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; - std::vector emissions; + PendingEmission emission; + emission.descriptor = descriptor; { std::lock_guard lock{state.mutex}; if (type == EventType::ServiceCreated) { - auto& entry = state.subscribers[key]; + auto& entry = state.tracked[key]; entry.descriptor = descriptor; entry.haveDescriptor = true; - if (entry.connections > 0 && !entry.announced) + emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; + emission.numberOfConnections = static_cast(entry.connections); + } + else if (type == EventType::ServiceRemoved) + { + state.tracked.erase(key); + for (auto cit = state.connectionParent.begin(); cit != state.connectionParent.end();) { - entry.announced = true; - emissions.push_back( - {SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, entry.descriptor}); + cit = (cit->second == key) ? state.connectionParent.erase(cit) : std::next(cit); } + emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; } - else if (type == EventType::ServiceRemoved) + } + EmitPending(state, emission); + return; + } + + // ---- DataSubscriberInternal: a confirmed pub/sub match; emit Service_Updated on the parent subscriber ---- + if (controllerType == Discovery::controllerTypeDataSubscriberInternal) + { + const ServiceKey internalKey{descriptor.GetParticipantName(), descriptor.GetServiceId()}; + std::string parentIdStr; + if (!descriptor.GetSupplementalDataItem(Discovery::supplKeyDataSubscriberInternalParentServiceID, parentIdStr)) + { + return; + } + SilKit::Core::EndpointId parentServiceId{0}; + try + { + parentServiceId = static_cast(std::stoull(parentIdStr)); + } + catch (...) + { + return; + } + const ServiceKey parentKey{descriptor.GetParticipantName(), parentServiceId}; + const std::string publisherUuid = descriptor.GetNetworkName(); + + std::vector emissions; + { + std::lock_guard lock{state.mutex}; + if (type == EventType::ServiceCreated) { - const auto it = state.subscribers.find(key); - if (it != state.subscribers.end()) + if (state.connectionParent.count(internalKey) == 0) { - if (it->second.announced) + state.connectionParent[internalKey] = parentKey; + auto& entry = state.tracked[parentKey]; + ++entry.connections; + if (entry.haveDescriptor) { - emissions.push_back( - {SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, it->second.descriptor}); + PendingEmission e; + e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; + e.descriptor = entry.descriptor; + e.numberOfConnections = static_cast(entry.connections); + const auto pubIt = state.publishersByUuid.find(publisherUuid); + if (pubIt != state.publishersByUuid.end()) + { + e.connectedParticipantName = pubIt->second.participantName; + e.connectedServiceName = pubIt->second.serviceName; + } + emissions.push_back(std::move(e)); } - state.subscribers.erase(it); - // Drop any still-open connections that referenced this subscriber. - for (auto cit = state.connectionParent.begin(); cit != state.connectionParent.end();) + } + } + else if (type == EventType::ServiceRemoved) + { + const auto cit = state.connectionParent.find(internalKey); + if (cit != state.connectionParent.end()) + { + const auto storedParentKey = cit->second; + state.connectionParent.erase(cit); + const auto sit = state.tracked.find(storedParentKey); + if (sit != state.tracked.end() && sit->second.connections > 0) { - cit = (cit->second == key) ? state.connectionParent.erase(cit) : std::next(cit); + --sit->second.connections; + if (sit->second.haveDescriptor) + { + PendingEmission e; + e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; + e.descriptor = sit->second.descriptor; + e.numberOfConnections = static_cast(sit->second.connections); + const auto pubIt = state.publishersByUuid.find(publisherUuid); + if (pubIt != state.publishersByUuid.end()) + { + e.connectedParticipantName = pubIt->second.participantName; + e.connectedServiceName = pubIt->second.serviceName; + } + emissions.push_back(std::move(e)); + } } } } } - for (const auto& emission : emissions) + for (const auto& e : emissions) { - Emit(state, emission.type, emission.descriptor); + EmitPending(state, e); } return; } - if (controllerType == Discovery::controllerTypeDataSubscriberInternal) + // ---- RpcClient: track UUID for RPC connection peer resolution; emit immediately ---- + if (controllerType == Discovery::controllerTypeRpcClient) + { + { + std::lock_guard lock{state.mutex}; + if (type == EventType::ServiceCreated) + { + state.rpcClientsByUuid[descriptor.GetNetworkName()] = {descriptor.GetParticipantName(), + descriptor.GetServiceName()}; + } + else if (type == EventType::ServiceRemoved) + { + state.rpcClientsByUuid.erase(descriptor.GetNetworkName()); + } + } + PendingEmission e; + e.type = ToC(type); + e.descriptor = descriptor; + EmitPending(state, e); + return; + } + + // ---- RpcServer: emit immediately; tracked for connection counting via RpcServerInternal ---- + if (controllerType == Discovery::controllerTypeRpcServer) + { + const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; + PendingEmission emission; + emission.descriptor = descriptor; + { + std::lock_guard lock{state.mutex}; + if (type == EventType::ServiceCreated) + { + auto& entry = state.tracked[key]; + entry.descriptor = descriptor; + entry.haveDescriptor = true; + emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; + emission.numberOfConnections = static_cast(entry.connections); + } + else if (type == EventType::ServiceRemoved) + { + state.tracked.erase(key); + for (auto cit = state.connectionParent.begin(); cit != state.connectionParent.end();) + { + cit = (cit->second == key) ? state.connectionParent.erase(cit) : std::next(cit); + } + emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; + } + } + EmitPending(state, emission); + return; + } + + // ---- RpcServerInternal: a confirmed RPC match; emit Service_Updated on the parent server ---- + if (controllerType == Discovery::controllerTypeRpcServerInternal) { const ServiceKey internalKey{descriptor.GetParticipantName(), descriptor.GetServiceId()}; std::string parentIdStr; - if (!descriptor.GetSupplementalDataItem(Discovery::supplKeyDataSubscriberInternalParentServiceID, parentIdStr)) + if (!descriptor.GetSupplementalDataItem(Discovery::supplKeyRpcServerInternalParentServiceID, parentIdStr)) { return; } @@ -323,6 +585,8 @@ void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent return; } const ServiceKey parentKey{descriptor.GetParticipantName(), parentServiceId}; + std::string clientUuid; + descriptor.GetSupplementalDataItem(Discovery::supplKeyRpcServerInternalClientUUID, clientUuid); std::vector emissions; { @@ -332,13 +596,21 @@ void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent if (state.connectionParent.count(internalKey) == 0) { state.connectionParent[internalKey] = parentKey; - auto& entry = state.subscribers[parentKey]; + auto& entry = state.tracked[parentKey]; ++entry.connections; - if (entry.haveDescriptor && !entry.announced) + if (entry.haveDescriptor) { - entry.announced = true; - emissions.push_back( - {SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, entry.descriptor}); + PendingEmission e; + e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; + e.descriptor = entry.descriptor; + e.numberOfConnections = static_cast(entry.connections); + const auto clientIt = state.rpcClientsByUuid.find(clientUuid); + if (clientIt != state.rpcClientsByUuid.end()) + { + e.connectedParticipantName = clientIt->second.participantName; + e.connectedServiceName = clientIt->second.serviceName; + } + emissions.push_back(std::move(e)); } } } @@ -349,35 +621,40 @@ void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent { const auto storedParentKey = cit->second; state.connectionParent.erase(cit); - const auto sit = state.subscribers.find(storedParentKey); - if (sit != state.subscribers.end() && sit->second.connections > 0) + const auto sit = state.tracked.find(storedParentKey); + if (sit != state.tracked.end() && sit->second.connections > 0) { --sit->second.connections; - if (sit->second.connections == 0 && sit->second.announced) + if (sit->second.haveDescriptor) { - sit->second.announced = false; - emissions.push_back( - {SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, sit->second.descriptor}); + PendingEmission e; + e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; + e.descriptor = sit->second.descriptor; + e.numberOfConnections = static_cast(sit->second.connections); + const auto clientIt = state.rpcClientsByUuid.find(clientUuid); + if (clientIt != state.rpcClientsByUuid.end()) + { + e.connectedParticipantName = clientIt->second.participantName; + e.connectedServiceName = clientIt->second.serviceName; + } + emissions.push_back(std::move(e)); } } } } } - for (const auto& emission : emissions) + for (const auto& e : emissions) { - Emit(state, emission.type, emission.descriptor); + EmitPending(state, e); } return; } - // Publishers, RPC clients/servers, bus controllers, network links, and anything else: reported on - // creation/removal exactly as before. - SilKit_Experimental_ServiceDescriptor reportedDescriptor{}; - LabelStorage storage; - if (ClassifyAndFill(descriptor, reportedDescriptor, storage)) - { - state.handler(state.context, ToC(type), &reportedDescriptor); - } + // Everything else (infrastructure, system services): forwarded as-is; ClassifyAndFill suppresses them. + PendingEmission e; + e.type = ToC(type); + e.descriptor = descriptor; + EmitPending(state, e); } } // namespace @@ -408,9 +685,6 @@ try auto* cppServiceDiscovery = reinterpret_cast(serviceDiscovery); - // The observer state is captured by (and thus owned by) the registered handler, which lives as - // long as the participant's service discovery. It buffers pub/sub matches and synthesizes - // connection-gated DataSubscriber events (see ObserverState / HandleDiscoveryEvent). auto state = std::make_shared(); state->handler = handler; state->context = context; diff --git a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp index 160339f20..09586abde 100644 --- a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp +++ b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp @@ -32,8 +32,9 @@ struct CapturedLabel SilKit_LabelKind kind; }; -// Captures, inside the handler invocation, everything the public struct carries. Copying into owned storage here also -// verifies that the struct and all its borrowed pointers are valid for the duration of the callback. +// Captures everything the public struct carries inside the handler invocation. Copying into owned +// storage also verifies that the struct and all its borrowed pointers are valid for the duration of +// the callback. struct CallbackData { int callCount{0}; @@ -45,6 +46,12 @@ struct CallbackData std::string primaryIdentifier; std::string mediaType; std::vector labels; + std::string simulationName; + uint32_t numberOfConnections{0}; + std::string connectedParticipantName; + std::string connectedServiceName; + bool isSimulated{false}; + std::string simulatingParticipantName; }; void SilKitCALL CapturingHandler(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type type, @@ -64,6 +71,12 @@ void SilKitCALL CapturingHandler(void* context, SilKit_Experimental_ServiceDisco const auto& label = serviceDescriptor->labelList.labels[i]; data->labels.push_back({label.key, label.value, label.kind}); } + data->simulationName = serviceDescriptor->simulationName; + data->numberOfConnections = serviceDescriptor->numberOfConnections; + data->connectedParticipantName = serviceDescriptor->connectedParticipantName; + data->connectedServiceName = serviceDescriptor->connectedServiceName; + data->isSimulated = serviceDescriptor->isSimulated != SilKit_False; + data->simulatingParticipantName = serviceDescriptor->simulatingParticipantName; } void SilKitCALL NoopHandler(void* /*context*/, SilKit_Experimental_ServiceDiscoveryEvent_Type /*type*/, @@ -153,6 +166,9 @@ TEST_F(Test_CapiServiceDiscovery, reports_can_controller) EXPECT_EQ(data.primaryIdentifier, "CAN1"); // bus controller: identifier is the network name EXPECT_EQ(data.mediaType, ""); EXPECT_TRUE(data.labels.empty()); + EXPECT_EQ(data.numberOfConnections, 0u); + EXPECT_FALSE(data.isSimulated); + EXPECT_EQ(data.simulatingParticipantName, ""); } TEST_F(Test_CapiServiceDiscovery, reports_data_publisher_with_decoded_metadata) @@ -198,8 +214,10 @@ TEST_F(Test_CapiServiceDiscovery, reports_rpc_client_with_function_name) EXPECT_EQ(data.mediaType, "application/octet-stream"); } -TEST_F(Test_CapiServiceDiscovery, reports_network_link) +TEST_F(Test_CapiServiceDiscovery, network_link_events_are_suppressed) { + // Network links are not reported directly at the public API level. Instead they drive + // isSimulated / Service_Updated events on bus controllers (see is_simulated_* tests below). CallbackData data; auto handler = InstallHandler(data); @@ -211,9 +229,7 @@ TEST_F(Test_CapiServiceDiscovery, reports_network_link) handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); - EXPECT_EQ(data.callCount, 1); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_NetworkLink); - EXPECT_EQ(data.primaryIdentifier, "CAN1"); + EXPECT_EQ(data.callCount, 0); } TEST_F(Test_CapiServiceDiscovery, reports_removal) @@ -236,7 +252,7 @@ TEST_F(Test_CapiServiceDiscovery, filters_infrastructure_and_internal_services) // Infrastructure controller (SystemMonitor) is not user-facing. handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeController(Discovery::controllerTypeSystemMonitor, "default", "SystemMonitor")); - // Internal pub/sub helper is not user-facing. + // DataSubscriberInternal with no parent id is silently dropped. handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeController(Discovery::controllerTypeDataSubscriberInternal, "sub-uuid", "SubInternal")); @@ -267,13 +283,11 @@ TEST_F(Test_CapiServiceDiscovery, malformed_labels_are_swallowed) } // -------------------------------------------------------------------------------------------------- -// Connection-gated DataSubscriber synthesis: a user-facing DataSubscriber is reported only while it -// has at least one confirmed connection (a DataSubscriberInternal announcement whose parent-service -// id points back at it). Internal services and the transport UUID are never exposed. +// DataSubscriber: reported immediately on creation; Service_Updated fires on each connection change. // -------------------------------------------------------------------------------------------------- namespace { -// User-facing DataSubscriber with an explicit service id and topic (topic drives primaryIdentifier). + auto MakeUserSubscriber(SilKit::Core::EndpointId serviceId, const std::string& topic) -> ServiceDescriptor { ServiceDescriptor descriptor; @@ -287,14 +301,15 @@ auto MakeUserSubscriber(SilKit::Core::EndpointId serviceId, const std::string& t return descriptor; } -// DataSubscriberInternal (a confirmed connection) whose parent is the user-facing subscriber above. -auto MakeInternalConnection(SilKit::Core::EndpointId serviceId, SilKit::Core::EndpointId parentServiceId) - -> ServiceDescriptor +// DataSubscriberInternal: a confirmed match between a publisher (identified by its UUID = networkName) +// and the parent DataSubscriber. +auto MakeInternalConnection(SilKit::Core::EndpointId serviceId, SilKit::Core::EndpointId parentServiceId, + const std::string& publisherUuid = "pub-uuid") -> ServiceDescriptor { ServiceDescriptor descriptor; descriptor.SetParticipantNameAndComputeId("SubParticipant"); descriptor.SetServiceName("SubInternal"); - descriptor.SetNetworkName("pub-uuid"); + descriptor.SetNetworkName(publisherUuid); descriptor.SetServiceType(ServiceType::Controller); descriptor.SetServiceId(serviceId); descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataSubscriberInternal); @@ -302,83 +317,377 @@ auto MakeInternalConnection(SilKit::Core::EndpointId serviceId, SilKit::Core::En std::to_string(parentServiceId)); return descriptor; } + +auto MakeDataPublisher(const std::string& publisherUuid, const std::string& participantName, + const std::string& serviceName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(participantName); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(publisherUuid); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataPublisher); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "T"); + return descriptor; +} + +auto MakeRpcServer(SilKit::Core::EndpointId serviceId, const std::string& functionName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ServerParticipant"); + descriptor.SetServiceName("RpcSrv"); + descriptor.SetNetworkName("srv-net"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcServer); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerFunctionName, functionName); + return descriptor; +} + +auto MakeRpcClient(const std::string& clientUuid, const std::string& participantName, + const std::string& serviceName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(participantName); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(clientUuid); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcClient); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientFunctionName, "Add"); + return descriptor; +} + +auto MakeRpcServerInternal(SilKit::Core::EndpointId serviceId, SilKit::Core::EndpointId parentServiceId, + const std::string& clientUuid) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ServerParticipant"); + descriptor.SetServiceName("RpcSrvInternal"); + descriptor.SetNetworkName("srv-internal-net"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcServerInternal); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerInternalParentServiceID, + std::to_string(parentServiceId)); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerInternalClientUUID, clientUuid); + return descriptor; +} + +auto MakeBusController(const std::string& ctrlType, const std::string& networkName, const std::string& serviceName, + SilKit::Core::EndpointId serviceId, + const std::string& participantName = "CtrlParticipant") -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(participantName); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(networkName); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, ctrlType); + return descriptor; +} + +auto MakeNetworkLink(const std::string& networkName, const std::string& simulatorParticipant) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(simulatorParticipant); + descriptor.SetServiceName(networkName + "_link"); + descriptor.SetNetworkName(networkName); + descriptor.SetServiceType(ServiceType::Link); + return descriptor; +} + } // namespace -TEST_F(Test_CapiServiceDiscovery, subscriber_without_connection_is_not_reported) +TEST_F(Test_CapiServiceDiscovery, subscriber_reported_immediately_on_creation) { + // A DataSubscriber is visible as soon as it is created, even without any connections. CallbackData data; auto handler = InstallHandler(data); handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); - EXPECT_EQ(data.callCount, 0); + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + EXPECT_EQ(data.primaryIdentifier, "T"); + EXPECT_EQ(data.numberOfConnections, 0u); + EXPECT_EQ(data.connectedParticipantName, ""); + EXPECT_EQ(data.connectedServiceName, ""); } -TEST_F(Test_CapiServiceDiscovery, subscriber_reported_when_connection_appears) +TEST_F(Test_CapiServiceDiscovery, subscriber_service_updated_on_connection) { + // When a DataSubscriberInternal confirms a match, a Service_Updated with the new count fires. CallbackData data; auto handler = InstallHandler(data); handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + ASSERT_EQ(data.callCount, 1); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); - EXPECT_EQ(data.callCount, 1); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.callCount, 2); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); EXPECT_EQ(data.primaryIdentifier, "T"); + EXPECT_EQ(data.numberOfConnections, 1u); } -TEST_F(Test_CapiServiceDiscovery, subscriber_reported_when_connection_arrives_before_subscriber) +TEST_F(Test_CapiServiceDiscovery, subscriber_service_updated_shows_peer_info) { + // The Service_Updated event for a DataSubscriber connection names the publisher as the peer. CallbackData data; auto handler = InstallHandler(data); - // Reversed order: the connection is discovered before the user-facing subscriber. - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); - EXPECT_EQ(data.callCount, 0); - + const std::string uuid = "pub-uuid-abc"; + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeDataPublisher(uuid, "PubParticipant", "MyPub")); handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); - EXPECT_EQ(data.callCount, 1); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + + ASSERT_EQ(data.callCount, 3); // publisher + subscriber + service_updated + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_EQ(data.connectedParticipantName, "PubParticipant"); + EXPECT_EQ(data.connectedServiceName, "MyPub"); + EXPECT_EQ(data.numberOfConnections, 1u); } -TEST_F(Test_CapiServiceDiscovery, subscriber_removed_when_connection_removed) +TEST_F(Test_CapiServiceDiscovery, subscriber_service_updated_on_disconnection) { + // When the DataSubscriberInternal is removed the count decrements via another Service_Updated. CallbackData data; auto handler = InstallHandler(data); handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); - ASSERT_EQ(data.callCount, 1); + ASSERT_EQ(data.callCount, 2); handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(200, 100)); - EXPECT_EQ(data.callCount, 2); + EXPECT_EQ(data.callCount, 3); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_EQ(data.numberOfConnections, 0u); +} + +TEST_F(Test_CapiServiceDiscovery, subscriber_service_removed_on_removal) +{ + // ServiceRemoved fires when the DataSubscriber itself is removed, not on last-connection loss. + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); + ASSERT_EQ(data.callCount, 2); + + handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeUserSubscriber(100, "T")); + + EXPECT_EQ(data.callCount, 3); EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); } -TEST_F(Test_CapiServiceDiscovery, subscriber_reported_once_for_multiple_connections) +TEST_F(Test_CapiServiceDiscovery, subscriber_connection_arrives_before_subscriber) { + // When the DataSubscriberInternal arrives before the DataSubscriber, the subscriber is reported + // as ServiceCreated (with connections already counted) once the subscriber itself arrives. CallbackData data; auto handler = InstallHandler(data); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); + EXPECT_EQ(data.callCount, 0); // subscriber not known yet + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + EXPECT_EQ(data.numberOfConnections, 1u); // connection already counted +} + +TEST_F(Test_CapiServiceDiscovery, subscriber_multiple_connections_counted) +{ + // Each new DataSubscriberInternal fires a Service_Updated with an incremented count. + CallbackData data; + auto handler = InstallHandler(data); - // Two publishers => two internal connections for the same subscriber: only one ServiceCreated. + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); + EXPECT_EQ(data.numberOfConnections, 1u); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(201, 100)); - EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.callCount, 3); + EXPECT_EQ(data.numberOfConnections, 2u); - // Removing one connection keeps the subscriber connected (no event). handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(200, 100)); - EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.numberOfConnections, 1u); - // Removing the last connection reports the subscriber removed. handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(201, 100)); + EXPECT_EQ(data.numberOfConnections, 0u); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_EQ(data.callCount, 5); +} + +// -------------------------------------------------------------------------------------------------- +// RpcServer: reported immediately on creation; Service_Updated fires on each RpcServerInternal event. +// -------------------------------------------------------------------------------------------------- + +TEST_F(Test_CapiServiceDiscovery, rpc_server_reported_immediately_on_creation) +{ + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_RpcServer); + EXPECT_EQ(data.primaryIdentifier, "Add"); + EXPECT_EQ(data.numberOfConnections, 0u); +} + +TEST_F(Test_CapiServiceDiscovery, rpc_server_service_updated_on_connection) +{ + // When a RpcServerInternal confirms a call match, a Service_Updated fires on the parent server. + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + ASSERT_EQ(data.callCount, 1); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, "client-uuid-1")); + EXPECT_EQ(data.callCount, 2); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_RpcServer); + EXPECT_EQ(data.numberOfConnections, 1u); +} + +TEST_F(Test_CapiServiceDiscovery, rpc_server_service_updated_shows_peer_info) +{ + // The Service_Updated for an RPC connection names the client as the peer. + CallbackData data; + auto handler = InstallHandler(data); + + const std::string clientUuid = "client-uuid-xyz"; + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcClient(clientUuid, "ClientParticipant", "MyClient")); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, clientUuid)); + + ASSERT_EQ(data.callCount, 3); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_EQ(data.connectedParticipantName, "ClientParticipant"); + EXPECT_EQ(data.connectedServiceName, "MyClient"); + EXPECT_EQ(data.numberOfConnections, 1u); +} + +TEST_F(Test_CapiServiceDiscovery, rpc_server_service_updated_on_disconnection) +{ + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, "client-uuid")); + ASSERT_EQ(data.callCount, 2); + + handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeRpcServerInternal(60, 50, "client-uuid")); + + EXPECT_EQ(data.callCount, 3); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_EQ(data.numberOfConnections, 0u); +} + +// -------------------------------------------------------------------------------------------------- +// Network simulator detection: bus controllers carry isSimulated / simulatingParticipantName. +// No NetworkLink event type is exposed at the public API level. +// -------------------------------------------------------------------------------------------------- + +TEST_F(Test_CapiServiceDiscovery, is_simulated_false_for_controller_without_link) +{ + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); + + EXPECT_EQ(data.callCount, 1); + EXPECT_FALSE(data.isSimulated); + EXPECT_EQ(data.simulatingParticipantName, ""); +} + +TEST_F(Test_CapiServiceDiscovery, is_simulated_set_when_link_arrives_after_controller) +{ + // When a network simulator announces itself after the bus controller, a Service_Updated fires. + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); + ASSERT_EQ(data.callCount, 1); + ASSERT_FALSE(data.isSimulated); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); + + EXPECT_EQ(data.callCount, 2); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_CanController); + EXPECT_TRUE(data.isSimulated); + EXPECT_EQ(data.simulatingParticipantName, "SimParticipant"); +} + +TEST_F(Test_CapiServiceDiscovery, is_simulated_set_on_service_created_when_link_precedes_controller) +{ + // When the network link is already active before the bus controller arrives, the ServiceCreated + // event for that controller already carries isSimulated = true — no separate Service_Updated needed. + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); + EXPECT_EQ(data.callCount, 0); // link itself not reported + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); + + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_TRUE(data.isSimulated); + EXPECT_EQ(data.simulatingParticipantName, "SimParticipant"); +} + +TEST_F(Test_CapiServiceDiscovery, is_simulated_cleared_on_link_removal) +{ + // When the network simulator disappears, a Service_Updated clears isSimulated. + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); + ASSERT_EQ(data.callCount, 2); + ASSERT_TRUE(data.isSimulated); + + handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeNetworkLink("CAN1", "SimParticipant")); + + EXPECT_EQ(data.callCount, 3); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_FALSE(data.isSimulated); + EXPECT_EQ(data.simulatingParticipantName, ""); +} + +TEST_F(Test_CapiServiceDiscovery, link_fan_out_reaches_all_controllers_on_network) +{ + // A single link event fans out to every bus controller on that network. + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can2", 2)); + ASSERT_EQ(data.callCount, 2); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); + + // Both controllers receive a Service_Updated. + EXPECT_EQ(data.callCount, 4); } } // namespace From ee4e588ec800c2952012719cc2e1d9bcdd0e4819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Tue, 21 Jul 2026 08:33:37 +0200 Subject: [PATCH 09/10] fixes from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- .../Test_HourglassServiceDiscovery.cpp | 39 ++ .../ITest_CapiServiceDiscovery.cpp | 53 +++ SilKit/include/silkit/capi/Experimental.h | 22 +- .../serviceDiscovery/ServiceDiscovery.hpp | 58 ++- .../serviceDiscovery/IServiceDiscovery.hpp | 15 +- SilKit/source/capi/CapiExperimental.cpp | 417 +++++++++++------- .../source/capi/Test_CapiServiceDiscovery.cpp | 109 ++++- 7 files changed, 540 insertions(+), 173 deletions(-) diff --git a/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp b/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp index 2ef4075ae..86b3233fc 100644 --- a/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp +++ b/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp @@ -150,6 +150,45 @@ TEST_F(Test_HourglassServiceDiscovery, service_discovery_handler_round_trip) EXPECT_TRUE(received.labels.empty()); } +// Setting the handler twice must register the C trampoline exactly once (no duplicate delivery) and +// replace the user handler in place without dangling the previously stored std::function. +TEST_F(Test_HourglassServiceDiscovery, set_handler_twice_replaces_and_registers_once) +{ + ServiceDiscoveryWrapper serviceDiscovery{mockParticipant}; + + void* capturedContext{nullptr}; + SilKit_Experimental_ServiceDiscoveryHandler_t capturedHandler{nullptr}; + EXPECT_CALL(capi, SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(mockServiceDiscovery, _, _)) + .Times(1) + .WillOnce(DoAll(SaveArg<1>(&capturedContext), SaveArg<2>(&capturedHandler), + Return(SilKit_ReturnCode_SUCCESS))); + + int firstCalls{0}; + int secondCalls{0}; + serviceDiscovery.SetServiceDiscoveryHandler( + [&](SD::ServiceDiscoveryEventType, const SD::ServiceDescriptor&) { ++firstCalls; }); + serviceDiscovery.SetServiceDiscoveryHandler( + [&](SD::ServiceDiscoveryEventType, const SD::ServiceDescriptor&) { ++secondCalls; }); + + ASSERT_NE(capturedHandler, nullptr); + + SilKit_Experimental_ServiceDescriptor cDescriptor; + SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, cDescriptor); + cDescriptor.participantName = "P"; + cDescriptor.serviceName = "S"; + cDescriptor.serviceKind = SilKit_Experimental_ServiceKind_CanController; + cDescriptor.primaryIdentifier = "CAN1"; + cDescriptor.mediaType = ""; + cDescriptor.labelList.numLabels = 0; + cDescriptor.labelList.labels = nullptr; + + capturedHandler(capturedContext, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, &cDescriptor); + + // Only the second (current) handler is invoked; the first was replaced, not left dangling. + EXPECT_EQ(firstCalls, 0); + EXPECT_EQ(secondCalls, 1); +} + TEST_F(Test_HourglassServiceDiscovery, to_string_maps_enums) { EXPECT_EQ(SD::to_string(SD::ServiceKind::DataSubscriber), "DataSubscriber"); diff --git a/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp index 1ebaa4789..c4c01dde9 100644 --- a/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp +++ b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp @@ -424,4 +424,57 @@ TEST_F(ITest_CapiServiceDiscovery, rpc_service_updated_carries_client_peer_ident << "server's Service_Updated must identify the RPC client's controller name"; } +// Attaching an observer to an already-running simulation must recover not only the connection count +// but also the peer identity of connections that existed before the observer registered. This is the +// replay-ordering case: the DataSubscriberInternal may be replayed before its parent/publisher. +TEST_F(ITest_CapiServiceDiscovery, late_observer_recovers_peer_of_preexisting_connection) +{ + const std::string topic{"LateTopic"}; + const std::string mediaType{"M"}; + PubSubSpec spec{topic, mediaType}; + + auto publisher = CreateParticipant("LatePub"); + auto subscriber = CreateParticipant("LateSub"); + publisher->CreateDataPublisher("LatePubCtrl", spec, 0); + subscriber->CreateDataSubscriber("LateSubCtrl", spec, [](IDataSubscriber*, const DataMessageEvent&) {}); + + // Ensure the match is established (observed via the SetUp observer) before attaching a new one. + ASSERT_TRUE(WaitFor([&] { + return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated + && e.primaryIdentifier == topic && e.numberOfConnections >= 1; + }); + })) << "the pub/sub connection was never established"; + + // Attach a brand-new observer that must replay the already-running simulation. + SilKit_ParticipantConfiguration* config{nullptr}; + ASSERT_EQ(SilKit_ParticipantConfiguration_FromString(&config, ""), SilKit_ReturnCode_SUCCESS); + SilKit_Participant* lateObserver{nullptr}; + ASSERT_EQ(SilKit_Participant_Create(&lateObserver, config, "LateObserver", _registryUri.c_str()), + SilKit_ReturnCode_SUCCESS); + SilKit_ParticipantConfiguration_Destroy(config); + + ObserverContext lateCtx; + SilKit_Experimental_ServiceDiscovery* lateSd{nullptr}; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&lateSd, lateObserver), SilKit_ReturnCode_SUCCESS); + ASSERT_EQ( + SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(lateSd, &lateCtx, &OnServiceDiscovery), + SilKit_ReturnCode_SUCCESS); + + std::unique_lock lock{lateCtx.mutex}; + const bool recovered = lateCtx.cv.wait_for(lock, kWaitTimeout, [&] { + return std::any_of(lateCtx.events.begin(), lateCtx.events.end(), [&](const auto& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated + && e.primaryIdentifier == topic && e.numberOfConnections >= 1 + && e.connectedParticipantName == "LatePub" && e.connectedServiceName == "LatePubCtrl"; + }); + }); + lock.unlock(); + EXPECT_TRUE(recovered) << "late observer must recover the peer identity of the pre-existing connection"; + + SilKit_Participant_Destroy(lateObserver); +} + } // namespace diff --git a/SilKit/include/silkit/capi/Experimental.h b/SilKit/include/silkit/capi/Experimental.h index 1bf8170ff..9dfb19240 100644 --- a/SilKit/include/silkit/capi/Experimental.h +++ b/SilKit/include/silkit/capi/Experimental.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH // // SPDX-License-Identifier: MIT @@ -87,7 +87,7 @@ typedef struct const char* mediaType; //! Decoded matching labels for pub/sub and RPC services; empty for bus controllers. SilKit_LabelList labelList; - //! The simulation name this service belongs to. Empty string when not available. + //! Reserved for future system-level simulation detection. Currently always an empty string. const char* simulationName; //! Name of the peer participant; populated only in ServiceUpdated connection events. const char* connectedParticipantName; @@ -95,7 +95,9 @@ typedef struct const char* connectedServiceName; //! Name of the participant simulating this controller's network; empty when not simulated. const char* simulatingParticipantName; - //! Number of active matched connections (pub/sub matches or RPC call pairs); 0 for bus controllers. + //! Number of active matched connections. Reported on the receiving side only: DataSubscribers count + //! matched publishers and RpcServers count matched clients. Always 0 for DataPublishers, RpcClients + //! and bus controllers. uint32_t numberOfConnections; //! True when a network simulator owns this bus controller's network. SilKit_Bool isSimulated; @@ -106,6 +108,12 @@ typedef struct * The \p serviceDescriptor and all of its pointer members are only valid for the duration of the * handler invocation. Copy the data if it must outlive the call. * + * \note Threading: this handler may be invoked on an internal SIL Kit worker thread or on an + * application thread that creates or destroys a service; the invoking thread is unspecified. + * Invocations are serialized (the handler is never called concurrently with itself). The + * handler must not block and must not call back into the participant that owns the observer, + * as doing so may deadlock. + * * \param context The user context pointer passed to \ref SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler. * \param eventType Whether the service was created, updated, or removed. * \param serviceDescriptor The affected service. @@ -116,7 +124,9 @@ typedef void(SilKitFPTR* SilKit_Experimental_ServiceDiscoveryHandler_t)( /*! \brief Obtain the experimental service discovery observer of a participant. * - * The returned object is owned by the participant and must not be destroyed by the caller. + * The returned object is owned by the participant and must not be destroyed by the caller; there is + * no corresponding destroy function. It refers to the participant's single service discovery, so + * repeated calls for the same participant yield the same observer handle. * * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without * prior notice. @@ -139,6 +149,10 @@ typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDiscovery_Creat * instead, affected bus controllers receive a * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated with \p isSimulated set. * + * \note Each call registers an additional, independent handler; handlers cannot be removed and remain + * registered for the lifetime of the participant. To observe with a single handler, call this + * function once. See the handler typedef for the threading contract. + * * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without * prior notice. * diff --git a/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp b/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp index 7e06f075e..06b65e1d4 100644 --- a/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp +++ b/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp @@ -35,7 +35,13 @@ class ServiceDiscovery : public SilKit::Experimental::ServiceDiscovery::IService private: SilKit_Experimental_ServiceDiscovery* _serviceDiscovery{nullptr}; - std::unique_ptr _handler; + // The handler is stored in a slot whose address is passed to the C API as the callback context and + // must therefore remain stable for the lifetime of this object. Repeated calls to + // SetServiceDiscoveryHandler replace the stored std::function in place (never freeing the slot) and + // register the C trampoline exactly once, so the context can never dangle and events are not + // delivered twice. See SetServiceDiscoveryHandler below. + std::shared_ptr _handler; + bool _registered{false}; }; } // namespace ServiceDiscovery @@ -64,45 +70,63 @@ ServiceDiscovery::ServiceDiscovery(SilKit_Participant* participant) void ServiceDiscovery::SetServiceDiscoveryHandler( SilKit::Experimental::ServiceDiscovery::ServiceDiscoveryHandler handler) { - auto ownedHandler = - std::make_unique(std::move(handler)); + // Store the handler in a slot with a stable address (used as the C callback context). On repeated + // calls we swap the std::function in place instead of freeing and re-registering, which would leave + // the previously registered C trampoline pointing at a destroyed std::function (use-after-free). + if (_handler == nullptr) + { + _handler = std::make_shared(); + } + *_handler = std::move(handler); + + if (_registered) + { + // The trampoline reads the current value of *_handler on every invocation, so the replacement + // above already takes effect; registering again would duplicate event delivery. + return; + } const auto cHandler = [](void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type eventType, const SilKit_Experimental_ServiceDescriptor* cServiceDescriptor) { namespace SD = SilKit::Experimental::ServiceDiscovery; + const auto orEmpty = [](const char* s) -> const char* { return s != nullptr ? s : ""; }; + SD::ServiceDescriptor serviceDescriptor{}; - serviceDescriptor.participantName = cServiceDescriptor->participantName; - serviceDescriptor.serviceName = cServiceDescriptor->serviceName; + serviceDescriptor.participantName = orEmpty(cServiceDescriptor->participantName); + serviceDescriptor.serviceName = orEmpty(cServiceDescriptor->serviceName); serviceDescriptor.serviceKind = static_cast(cServiceDescriptor->serviceKind); - serviceDescriptor.primaryIdentifier = cServiceDescriptor->primaryIdentifier; - serviceDescriptor.mediaType = cServiceDescriptor->mediaType; + serviceDescriptor.primaryIdentifier = orEmpty(cServiceDescriptor->primaryIdentifier); + serviceDescriptor.mediaType = orEmpty(cServiceDescriptor->mediaType); serviceDescriptor.labels.reserve(cServiceDescriptor->labelList.numLabels); for (size_t i = 0; i < cServiceDescriptor->labelList.numLabels; ++i) { const auto& cLabel = cServiceDescriptor->labelList.labels[i]; SilKit::Services::MatchingLabel label; - label.key = cLabel.key; - label.value = cLabel.value; + label.key = orEmpty(cLabel.key); + label.value = orEmpty(cLabel.value); label.kind = static_cast(cLabel.kind); serviceDescriptor.labels.emplace_back(std::move(label)); } - serviceDescriptor.simulationName = cServiceDescriptor->simulationName ? cServiceDescriptor->simulationName : ""; - serviceDescriptor.connectedParticipantName = cServiceDescriptor->connectedParticipantName ? cServiceDescriptor->connectedParticipantName : ""; - serviceDescriptor.connectedServiceName = cServiceDescriptor->connectedServiceName ? cServiceDescriptor->connectedServiceName : ""; - serviceDescriptor.simulatingParticipantName = cServiceDescriptor->simulatingParticipantName ? cServiceDescriptor->simulatingParticipantName : ""; + serviceDescriptor.simulationName = orEmpty(cServiceDescriptor->simulationName); + serviceDescriptor.connectedParticipantName = orEmpty(cServiceDescriptor->connectedParticipantName); + serviceDescriptor.connectedServiceName = orEmpty(cServiceDescriptor->connectedServiceName); + serviceDescriptor.simulatingParticipantName = orEmpty(cServiceDescriptor->simulatingParticipantName); serviceDescriptor.numberOfConnections = cServiceDescriptor->numberOfConnections; serviceDescriptor.isSimulated = cServiceDescriptor->isSimulated != SilKit_False; - (*static_cast(context))( - static_cast(eventType), serviceDescriptor); + auto& userHandler = *static_cast(context); + if (userHandler) + { + userHandler(static_cast(eventType), serviceDescriptor); + } }; const auto returnCode = - SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(_serviceDiscovery, ownedHandler.get(), cHandler); + SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(_serviceDiscovery, _handler.get(), cHandler); ThrowOnError(returnCode); - _handler = std::move(ownedHandler); + _registered = true; } auto ServiceDiscovery::Get() const -> SilKit_Experimental_ServiceDiscovery* diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp index 78b08e870..d08563f71 100644 --- a/SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp +++ b/SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp @@ -25,10 +25,19 @@ class IServiceDiscovery * * Upon registration the handler is immediately invoked once for every service that is already * known, each reported as \ref ServiceDiscoveryEventType::ServiceCreated. It is subsequently - * invoked for every user-facing service created or removed. Internal / infrastructure services - * are not reported. + * invoked for every user-facing service created, updated, or removed. Internal / infrastructure + * services are not reported. * - * \param handler The handler to be called on service creation and removal. + * Calling this method again replaces the handler; the previously set handler is no longer + * invoked. The replacement does not re-deliver the initial snapshot of already-known services. + * + * \note Threading: the handler may be invoked on an internal SIL Kit worker thread or on an + * application thread that creates or destroys a service; the invoking thread is + * unspecified. Invocations are serialized (never concurrent). The handler must not block + * and must not call back into the participant (doing so may deadlock). Copy any data that + * must outlive the call. + * + * \param handler The handler to be called on service creation, update, and removal. */ virtual void SetServiceDiscoveryHandler(ServiceDiscoveryHandler handler) = 0; }; diff --git a/SilKit/source/capi/CapiExperimental.cpp b/SilKit/source/capi/CapiExperimental.cpp index e0f95ed39..8d2517c0f 100644 --- a/SilKit/source/capi/CapiExperimental.cpp +++ b/SilKit/source/capi/CapiExperimental.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH // // SPDX-License-Identifier: MIT @@ -218,12 +218,24 @@ struct PeerInfo std::string serviceName; }; -// Tracks a user-facing service (DataSubscriber or RpcServer) for connection counting. +// Tracks a user-facing service (DataSubscriber or RpcServer) for connection counting and peer reveal. +// connKeys holds the internal-connection endpoints attached to this parent; its size is the connection +// count. Endpoints may be recorded before the parent's own descriptor is known (out-of-order replay), +// in which case haveDescriptor is false until the parent is discovered. struct TrackedEntry { SilKit::Core::ServiceDescriptor descriptor; bool haveDescriptor{false}; - std::size_t connections{0}; + std::set connKeys; +}; + +// A confirmed connection endpoint (DataSubscriberInternal / RpcServerInternal): links the internal +// endpoint to its parent (user-facing subscriber/server) and remembers the peer's UUID (the +// DataPublisher / RpcClient networkName) so the peer identity can be resolved, possibly later. +struct ConnInfo +{ + ServiceKey parentKey; + std::string peerUuid; }; // A synthesized event, queued while the state lock is held and dispatched after it is released. @@ -250,13 +262,14 @@ struct ObserverState // DataSubscribers and RpcServers tracked for connection counting and peer resolution. std::map tracked; - // internal-connection key -> parent (subscriber/server) key - std::map connectionParent; - - // DataPublisher UUID (= networkName of DataPublisher) -> peer info, for resolving pub/sub connections. - std::map publishersByUuid; - // RpcClient UUID (= networkName of RpcClient) -> peer info, for resolving RPC connections. - std::map rpcClientsByUuid; + // Internal-connection endpoint key -> connection info (parent key + peer UUID). + std::map connections; + // Peer UUID -> connection endpoints whose peer identity is not yet known, awaiting the peer's own + // discovery. Enables revealing the peer of a connection seen before its DataPublisher / RpcClient. + std::map> pendingByPeerUuid; + // DataPublisher / RpcClient UUID (= their networkName) -> peer identity. Keyed by globally unique + // UUID, so publishers and clients share this map without collision. + std::map peersByUuid; // Bus controllers by ServiceKey -> stored descriptor, for link-event fan-out. std::map busControllers; @@ -283,6 +296,189 @@ void EmitPending(ObserverState& state, const PendingEmission& e) state.handler(state.context, e.type, &out); } +// ---- Connection bookkeeping helpers. All are called with state.mutex held and append synthesized +// ServiceUpdated emissions for the *parent* service (subscriber/server); the emissions are always +// dispatched by the caller after the lock is released. Peer identity is filled in when known, and +// revealed via a later ServiceUpdated once the peer (publisher/client) is discovered. This keeps +// the connection graph reconstructable regardless of discovery/replay ordering. ---- + +auto MakeParentUpdate(const TrackedEntry& entry, const PeerInfo* peer) -> PendingEmission +{ + PendingEmission e; + e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; + e.descriptor = entry.descriptor; + e.numberOfConnections = static_cast(entry.connKeys.size()); + if (peer != nullptr) + { + e.connectedParticipantName = peer->participantName; + e.connectedServiceName = peer->serviceName; + } + return e; +} + +// A parent (DataSubscriber / RpcServer) was created: emit its ServiceCreated and reveal the peers of +// any connections already recorded against it (possible when discovery replays out of order). +void OnParentCreated(ObserverState& state, const ServiceKey& parentKey, + const SilKit::Core::ServiceDescriptor& descriptor, std::vector& emissions) +{ + auto& entry = state.tracked[parentKey]; + entry.descriptor = descriptor; + entry.haveDescriptor = true; + + PendingEmission created; + created.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; + created.descriptor = descriptor; + created.numberOfConnections = static_cast(entry.connKeys.size()); + emissions.push_back(std::move(created)); + + for (const auto& connKey : entry.connKeys) + { + const auto cit = state.connections.find(connKey); + if (cit == state.connections.end()) + { + continue; + } + const auto pit = state.peersByUuid.find(cit->second.peerUuid); + if (pit != state.peersByUuid.end()) + { + emissions.push_back(MakeParentUpdate(entry, &pit->second)); + } + else + { + state.pendingByPeerUuid[cit->second.peerUuid].insert(connKey); + } + } +} + +// A parent (DataSubscriber / RpcServer) was removed: drop it and all of its connection bookkeeping. +void OnParentRemoved(ObserverState& state, const ServiceKey& parentKey) +{ + const auto tit = state.tracked.find(parentKey); + if (tit == state.tracked.end()) + { + return; + } + for (const auto& connKey : tit->second.connKeys) + { + const auto cit = state.connections.find(connKey); + if (cit == state.connections.end()) + { + continue; + } + const auto pit = state.pendingByPeerUuid.find(cit->second.peerUuid); + if (pit != state.pendingByPeerUuid.end()) + { + pit->second.erase(connKey); + if (pit->second.empty()) + { + state.pendingByPeerUuid.erase(pit); + } + } + state.connections.erase(cit); + } + state.tracked.erase(tit); +} + +// A confirmed connection endpoint (DataSubscriberInternal / RpcServerInternal) appeared. +void OnConnectionCreated(ObserverState& state, const ServiceKey& connKey, const ServiceKey& parentKey, + const std::string& peerUuid, std::vector& emissions) +{ + if (state.connections.count(connKey) != 0) + { + return; // already counted + } + state.connections[connKey] = ConnInfo{parentKey, peerUuid}; + auto& entry = state.tracked[parentKey]; + entry.connKeys.insert(connKey); + + if (!entry.haveDescriptor) + { + return; // revealed when the parent itself is discovered + } + const auto pit = state.peersByUuid.find(peerUuid); + if (pit != state.peersByUuid.end()) + { + emissions.push_back(MakeParentUpdate(entry, &pit->second)); + } + else + { + // Surface the new connection count now; reveal the peer once its publisher/client is known. + emissions.push_back(MakeParentUpdate(entry, nullptr)); + state.pendingByPeerUuid[peerUuid].insert(connKey); + } +} + +// A confirmed connection endpoint was removed (a match dissolved). +void OnConnectionRemoved(ObserverState& state, const ServiceKey& connKey, std::vector& emissions) +{ + const auto cit = state.connections.find(connKey); + if (cit == state.connections.end()) + { + return; + } + const auto parentKey = cit->second.parentKey; + const auto peerUuid = cit->second.peerUuid; + state.connections.erase(cit); + + const auto pit = state.pendingByPeerUuid.find(peerUuid); + if (pit != state.pendingByPeerUuid.end()) + { + pit->second.erase(connKey); + if (pit->second.empty()) + { + state.pendingByPeerUuid.erase(pit); + } + } + + const auto tit = state.tracked.find(parentKey); + if (tit == state.tracked.end()) + { + return; + } + tit->second.connKeys.erase(connKey); + if (tit->second.haveDescriptor) + { + const auto peerIt = state.peersByUuid.find(peerUuid); + const PeerInfo* peer = peerIt != state.peersByUuid.end() ? &peerIt->second : nullptr; + emissions.push_back(MakeParentUpdate(tit->second, peer)); + } +} + +// A peer (DataPublisher / RpcClient) appeared: remember its identity and reveal it to any connection +// that was recorded before the peer was known. +void OnPeerCreated(ObserverState& state, const std::string& peerUuid, PeerInfo peerInfo, + std::vector& emissions) +{ + state.peersByUuid[peerUuid] = std::move(peerInfo); + const auto pit = state.pendingByPeerUuid.find(peerUuid); + if (pit == state.pendingByPeerUuid.end()) + { + return; + } + const auto& peer = state.peersByUuid[peerUuid]; + for (const auto& connKey : pit->second) + { + const auto cit = state.connections.find(connKey); + if (cit == state.connections.end()) + { + continue; + } + const auto tit = state.tracked.find(cit->second.parentKey); + if (tit != state.tracked.end() && tit->second.haveDescriptor) + { + emissions.push_back(MakeParentUpdate(tit->second, &peer)); + } + } + state.pendingByPeerUuid.erase(pit); +} + +// A peer (DataPublisher / RpcClient) was removed: forget its identity. Existing connection edges have +// already been reported; the connection's own removal (if any) reports the decremented count. +void OnPeerRemoved(ObserverState& state, const std::string& peerUuid) +{ + state.peersByUuid.erase(peerUuid); +} + // Handles a single internal discovery event, translating it into zero or more public emissions. // Emissions are always dispatched outside the state lock so user code never runs while we hold it. void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent::Type type, @@ -379,59 +575,64 @@ void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent return; } - // ---- DataPublisher: track UUID for connection peer resolution; emit immediately ---- + // ---- DataPublisher: a user-facing service AND a connection peer. Emit its own event first, then + // reveal it on any subscriber connection recorded before this publisher was known. ---- if (controllerType == Discovery::controllerTypeDataPublisher) { + std::vector reveals; { std::lock_guard lock{state.mutex}; if (type == EventType::ServiceCreated) { - state.publishersByUuid[descriptor.GetNetworkName()] = {descriptor.GetParticipantName(), - descriptor.GetServiceName()}; + OnPeerCreated(state, descriptor.GetNetworkName(), + PeerInfo{descriptor.GetParticipantName(), descriptor.GetServiceName()}, reveals); } else if (type == EventType::ServiceRemoved) { - state.publishersByUuid.erase(descriptor.GetNetworkName()); + OnPeerRemoved(state, descriptor.GetNetworkName()); } } - PendingEmission e; - e.type = ToC(type); - e.descriptor = descriptor; - EmitPending(state, e); + PendingEmission own; + own.type = ToC(type); + own.descriptor = descriptor; + EmitPending(state, own); + for (const auto& e : reveals) + { + EmitPending(state, e); + } return; } - // ---- DataSubscriber: emit immediately; tracked for connection counting via DataSubscriberInternal ---- + // ---- DataSubscriber: a tracked parent. OnParentCreated emits ServiceCreated and reveals any + // already-recorded connections; removal drops all of its bookkeeping. ---- if (controllerType == Discovery::controllerTypeDataSubscriber) { const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; - PendingEmission emission; - emission.descriptor = descriptor; + std::vector emissions; { std::lock_guard lock{state.mutex}; if (type == EventType::ServiceCreated) { - auto& entry = state.tracked[key]; - entry.descriptor = descriptor; - entry.haveDescriptor = true; - emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; - emission.numberOfConnections = static_cast(entry.connections); + OnParentCreated(state, key, descriptor, emissions); } else if (type == EventType::ServiceRemoved) { - state.tracked.erase(key); - for (auto cit = state.connectionParent.begin(); cit != state.connectionParent.end();) - { - cit = (cit->second == key) ? state.connectionParent.erase(cit) : std::next(cit); - } - emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; + OnParentRemoved(state, key); + PendingEmission removed; + removed.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; + removed.descriptor = descriptor; + emissions.push_back(std::move(removed)); } } - EmitPending(state, emission); + for (const auto& e : emissions) + { + EmitPending(state, e); + } return; } - // ---- DataSubscriberInternal: a confirmed pub/sub match; emit Service_Updated on the parent subscriber ---- + // ---- DataSubscriberInternal: a confirmed pub/sub match; drives Service_Updated on the parent + // subscriber, with the publisher (peerUuid = networkName) as the peer. ---- if (controllerType == Discovery::controllerTypeDataSubscriberInternal) { const ServiceKey internalKey{descriptor.GetParticipantName(), descriptor.GetServiceId()}; @@ -457,54 +658,11 @@ void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent std::lock_guard lock{state.mutex}; if (type == EventType::ServiceCreated) { - if (state.connectionParent.count(internalKey) == 0) - { - state.connectionParent[internalKey] = parentKey; - auto& entry = state.tracked[parentKey]; - ++entry.connections; - if (entry.haveDescriptor) - { - PendingEmission e; - e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; - e.descriptor = entry.descriptor; - e.numberOfConnections = static_cast(entry.connections); - const auto pubIt = state.publishersByUuid.find(publisherUuid); - if (pubIt != state.publishersByUuid.end()) - { - e.connectedParticipantName = pubIt->second.participantName; - e.connectedServiceName = pubIt->second.serviceName; - } - emissions.push_back(std::move(e)); - } - } + OnConnectionCreated(state, internalKey, parentKey, publisherUuid, emissions); } else if (type == EventType::ServiceRemoved) { - const auto cit = state.connectionParent.find(internalKey); - if (cit != state.connectionParent.end()) - { - const auto storedParentKey = cit->second; - state.connectionParent.erase(cit); - const auto sit = state.tracked.find(storedParentKey); - if (sit != state.tracked.end() && sit->second.connections > 0) - { - --sit->second.connections; - if (sit->second.haveDescriptor) - { - PendingEmission e; - e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; - e.descriptor = sit->second.descriptor; - e.numberOfConnections = static_cast(sit->second.connections); - const auto pubIt = state.publishersByUuid.find(publisherUuid); - if (pubIt != state.publishersByUuid.end()) - { - e.connectedParticipantName = pubIt->second.participantName; - e.connectedServiceName = pubIt->second.serviceName; - } - emissions.push_back(std::move(e)); - } - } - } + OnConnectionRemoved(state, internalKey, emissions); } } for (const auto& e : emissions) @@ -514,55 +672,59 @@ void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent return; } - // ---- RpcClient: track UUID for RPC connection peer resolution; emit immediately ---- + // ---- RpcClient: a user-facing service AND a connection peer. Emit its own event first, then + // reveal it on any server connection recorded before this client was known. ---- if (controllerType == Discovery::controllerTypeRpcClient) { + std::vector reveals; { std::lock_guard lock{state.mutex}; if (type == EventType::ServiceCreated) { - state.rpcClientsByUuid[descriptor.GetNetworkName()] = {descriptor.GetParticipantName(), - descriptor.GetServiceName()}; + OnPeerCreated(state, descriptor.GetNetworkName(), + PeerInfo{descriptor.GetParticipantName(), descriptor.GetServiceName()}, reveals); } else if (type == EventType::ServiceRemoved) { - state.rpcClientsByUuid.erase(descriptor.GetNetworkName()); + OnPeerRemoved(state, descriptor.GetNetworkName()); } } - PendingEmission e; - e.type = ToC(type); - e.descriptor = descriptor; - EmitPending(state, e); + PendingEmission own; + own.type = ToC(type); + own.descriptor = descriptor; + EmitPending(state, own); + for (const auto& e : reveals) + { + EmitPending(state, e); + } return; } - // ---- RpcServer: emit immediately; tracked for connection counting via RpcServerInternal ---- + // ---- RpcServer: a tracked parent. OnParentCreated emits ServiceCreated and reveals any + // already-recorded connections; removal drops all of its bookkeeping. ---- if (controllerType == Discovery::controllerTypeRpcServer) { const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; - PendingEmission emission; - emission.descriptor = descriptor; + std::vector emissions; { std::lock_guard lock{state.mutex}; if (type == EventType::ServiceCreated) { - auto& entry = state.tracked[key]; - entry.descriptor = descriptor; - entry.haveDescriptor = true; - emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; - emission.numberOfConnections = static_cast(entry.connections); + OnParentCreated(state, key, descriptor, emissions); } else if (type == EventType::ServiceRemoved) { - state.tracked.erase(key); - for (auto cit = state.connectionParent.begin(); cit != state.connectionParent.end();) - { - cit = (cit->second == key) ? state.connectionParent.erase(cit) : std::next(cit); - } - emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; + OnParentRemoved(state, key); + PendingEmission removed; + removed.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; + removed.descriptor = descriptor; + emissions.push_back(std::move(removed)); } } - EmitPending(state, emission); + for (const auto& e : emissions) + { + EmitPending(state, e); + } return; } @@ -593,54 +755,11 @@ void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent std::lock_guard lock{state.mutex}; if (type == EventType::ServiceCreated) { - if (state.connectionParent.count(internalKey) == 0) - { - state.connectionParent[internalKey] = parentKey; - auto& entry = state.tracked[parentKey]; - ++entry.connections; - if (entry.haveDescriptor) - { - PendingEmission e; - e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; - e.descriptor = entry.descriptor; - e.numberOfConnections = static_cast(entry.connections); - const auto clientIt = state.rpcClientsByUuid.find(clientUuid); - if (clientIt != state.rpcClientsByUuid.end()) - { - e.connectedParticipantName = clientIt->second.participantName; - e.connectedServiceName = clientIt->second.serviceName; - } - emissions.push_back(std::move(e)); - } - } + OnConnectionCreated(state, internalKey, parentKey, clientUuid, emissions); } else if (type == EventType::ServiceRemoved) { - const auto cit = state.connectionParent.find(internalKey); - if (cit != state.connectionParent.end()) - { - const auto storedParentKey = cit->second; - state.connectionParent.erase(cit); - const auto sit = state.tracked.find(storedParentKey); - if (sit != state.tracked.end() && sit->second.connections > 0) - { - --sit->second.connections; - if (sit->second.haveDescriptor) - { - PendingEmission e; - e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; - e.descriptor = sit->second.descriptor; - e.numberOfConnections = static_cast(sit->second.connections); - const auto clientIt = state.rpcClientsByUuid.find(clientUuid); - if (clientIt != state.rpcClientsByUuid.end()) - { - e.connectedParticipantName = clientIt->second.participantName; - e.connectedServiceName = clientIt->second.serviceName; - } - emissions.push_back(std::move(e)); - } - } - } + OnConnectionRemoved(state, internalKey, emissions); } } for (const auto& e : emissions) @@ -692,7 +811,9 @@ try cppServiceDiscovery->RegisterServiceDiscoveryHandler( [state](Discovery::ServiceDiscoveryEvent::Type type, const SilKit::Core::ServiceDescriptor& serviceDescriptor) { - // This runs on the SIL Kit IO worker thread. Exceptions must never propagate into SIL Kit internals. + // Invoked by the internal service discovery with its lock held, serialized, on an unspecified + // thread (an IO worker for remote events, or the caller's thread for locally created/removed + // services). Exceptions must never propagate into SIL Kit internals. try { HandleDiscoveryEvent(*state, type, serviceDescriptor); diff --git a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp index 09586abde..c0e576928 100644 --- a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp +++ b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH // // SPDX-License-Identifier: MIT @@ -526,6 +526,90 @@ TEST_F(Test_CapiServiceDiscovery, subscriber_multiple_connections_counted) EXPECT_EQ(data.callCount, 5); } +TEST_F(Test_CapiServiceDiscovery, subscriber_peer_revealed_when_publisher_arrives_late) +{ + // A connection observed before its publisher: the count is reported immediately without a peer, + // and the peer identity is revealed by a later Service_Updated once the publisher is discovered. + // This is what lets an observer that attaches to a running simulation reconstruct the graph. + CallbackData data; + auto handler = InstallHandler(data); + + const std::string uuid = "pub-uuid-late"; + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + + // Count is known, peer is not yet. + EXPECT_EQ(data.numberOfConnections, 1u); + EXPECT_EQ(data.connectedParticipantName, ""); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + + // The last event is the reveal: a Service_Updated on the subscriber naming the publisher. + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + EXPECT_EQ(data.numberOfConnections, 1u); + EXPECT_EQ(data.connectedParticipantName, "PubParticipant"); + EXPECT_EQ(data.connectedServiceName, "MyPub"); +} + +TEST_F(Test_CapiServiceDiscovery, subscriber_created_reveals_peer_for_preexisting_connection) +{ + // Adverse replay order (attach-to-running): the internal connection and the publisher are both + // seen before the subscriber. When the subscriber finally arrives, it is reported created and the + // pre-existing connection's peer is revealed on a following Service_Updated. + CallbackData data; + auto handler = InstallHandler(data); + + const std::string uuid = "pub-uuid-replay"; + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + EXPECT_EQ(data.numberOfConnections, 1u); + EXPECT_EQ(data.connectedParticipantName, "PubParticipant"); + EXPECT_EQ(data.connectedServiceName, "MyPub"); +} + +TEST_F(Test_CapiServiceDiscovery, reports_data_subscriber_with_decoded_labels) +{ + // The subscriber-side label key must be decoded too (not just the publisher's). + CallbackData data; + auto handler = InstallHandler(data); + + auto descriptor = MakeController(Discovery::controllerTypeDataSubscriber, "default", "MySubscriber"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberTopic, "TopicS"); + descriptor.SetSupplementalDataItem( + Discovery::supplKeyDataSubscriberSubLabels, + SerializeLabels({{"sk", "sv", SilKit::Services::MatchingLabel::Kind::Mandatory}})); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); + + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + EXPECT_EQ(data.primaryIdentifier, "TopicS"); + ASSERT_EQ(data.labels.size(), 1u); + EXPECT_EQ(data.labels[0].key, "sk"); + EXPECT_EQ(data.labels[0].value, "sv"); + EXPECT_EQ(data.labels[0].kind, SilKit_LabelKind_Mandatory); +} + +TEST_F(Test_CapiServiceDiscovery, reports_ethernet_and_flexray_controllers) +{ + CallbackData data; + auto handler = InstallHandler(data); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeEthernet, "ETH1", "Eth1")); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_EthernetController); + EXPECT_EQ(data.primaryIdentifier, "ETH1"); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeFlexray, "FR1", "Fr1")); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_FlexrayController); + EXPECT_EQ(data.primaryIdentifier, "FR1"); +} + // -------------------------------------------------------------------------------------------------- // RpcServer: reported immediately on creation; Service_Updated fires on each RpcServerInternal event. // -------------------------------------------------------------------------------------------------- @@ -579,6 +663,29 @@ TEST_F(Test_CapiServiceDiscovery, rpc_server_service_updated_shows_peer_info) EXPECT_EQ(data.numberOfConnections, 1u); } +TEST_F(Test_CapiServiceDiscovery, rpc_server_peer_revealed_when_client_arrives_late) +{ + // RPC analogue of the pub/sub late-peer reveal: the server connection is seen before the client; + // the client's identity is revealed by a later Service_Updated once the client is discovered. + CallbackData data; + auto handler = InstallHandler(data); + + const std::string clientUuid = "client-uuid-late"; + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, clientUuid)); + + EXPECT_EQ(data.numberOfConnections, 1u); + EXPECT_EQ(data.connectedParticipantName, ""); + + handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcClient(clientUuid, "ClientParticipant", "MyClient")); + + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_RpcServer); + EXPECT_EQ(data.numberOfConnections, 1u); + EXPECT_EQ(data.connectedParticipantName, "ClientParticipant"); + EXPECT_EQ(data.connectedServiceName, "MyClient"); +} + TEST_F(Test_CapiServiceDiscovery, rpc_server_service_updated_on_disconnection) { CallbackData data; From 82b26366f93320b49121cef53465405ad70e6c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Thu, 23 Jul 2026 09:20:50 +0200 Subject: [PATCH 10/10] refactor internal observer. add link events back in, they simplify the housekeeping alot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- .../Test_HourglassServiceDiscovery.cpp | 2 +- .../ITest_CapiServiceDiscovery.cpp | 143 ++-- .../ITest_NetSimServiceDiscovery.cpp | 59 +- SilKit/include/silkit/capi/Experimental.h | 60 +- .../serviceDiscovery/ServiceDiscovery.hpp | 3 - .../ServiceDiscoveryDatatypes.hpp | 30 +- .../serviceDiscovery/string_utils.hpp | 4 +- SilKit/source/capi/CMakeLists.txt | 3 + SilKit/source/capi/CapiExperimental.cpp | 758 +----------------- SilKit/source/capi/ServiceObserver.cpp | 424 ++++++++++ SilKit/source/capi/ServiceObserver.hpp | 100 +++ .../source/capi/Test_CapiServiceDiscovery.cpp | 725 +---------------- SilKit/source/capi/Test_ServiceObserver.cpp | 561 +++++++++++++ 13 files changed, 1260 insertions(+), 1612 deletions(-) create mode 100644 SilKit/source/capi/ServiceObserver.cpp create mode 100644 SilKit/source/capi/ServiceObserver.hpp create mode 100644 SilKit/source/capi/Test_ServiceObserver.cpp diff --git a/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp b/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp index 86b3233fc..163a675fc 100644 --- a/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp +++ b/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp @@ -193,9 +193,9 @@ TEST_F(Test_HourglassServiceDiscovery, to_string_maps_enums) { EXPECT_EQ(SD::to_string(SD::ServiceKind::DataSubscriber), "DataSubscriber"); EXPECT_EQ(SD::to_string(SD::ServiceKind::RpcServer), "RpcServer"); + EXPECT_EQ(SD::to_string(SD::ServiceKind::Link), "Link"); EXPECT_EQ(SD::to_string(SD::ServiceDiscoveryEventType::ServiceCreated), "ServiceCreated"); EXPECT_EQ(SD::to_string(SD::ServiceDiscoveryEventType::ServiceRemoved), "ServiceRemoved"); - EXPECT_EQ(SD::to_string(SD::ServiceDiscoveryEventType::ServiceUpdated), "ServiceUpdated"); } } // namespace diff --git a/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp index c4c01dde9..39731bcee 100644 --- a/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp +++ b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp @@ -6,11 +6,12 @@ // (SilKit_Experimental_ServiceDiscovery_*), driven against real participants over an // in-process registry. // -// Design: subscribers and RPC servers are reported immediately on ServiceCreated (no connection -// gating). When a DataSubscriberInternal confirms a pub/sub match, the parent DataSubscriber -// receives a Service_Updated event carrying the updated numberOfConnections and the peer's -// identity. ServiceRemoved fires only when the service itself is destroyed, not when a connection -// drops. +// Design: bus controllers, publishers/subscribers and RPC clients/servers are reported as their own +// service kinds on ServiceCreated/ServiceRemoved. A confirmed pub/sub or RPC match is reported as a +// SilKit_Experimental_ServiceKind_Link ServiceCreated event whose participantName/serviceName name +// the receiving side (subscriber/server) and whose connectedParticipantName/connectedServiceName +// name the peer (publisher/client). A match teardown emits no Link removal: it is inferred from the +// ServiceRemoved of one of the endpoints. #include #include @@ -60,7 +61,6 @@ struct ObservedEvent std::string primaryIdentifier; std::string mediaType; std::vector labels; - uint32_t numberOfConnections{0}; std::string connectedParticipantName; std::string connectedServiceName; }; @@ -92,7 +92,6 @@ void SilKitCALL OnServiceDiscovery(void* context, SilKit_Experimental_ServiceDis const auto& label = descriptor->labelList.labels[i]; event.labels.push_back({label.key, label.value, label.kind}); } - event.numberOfConnections = descriptor->numberOfConnections; event.connectedParticipantName = descriptor->connectedParticipantName ? descriptor->connectedParticipantName : ""; event.connectedServiceName = descriptor->connectedServiceName ? descriptor->connectedServiceName : ""; @@ -199,8 +198,6 @@ class ITest_CapiServiceDiscovery : public testing::Test // 1 publisher + 2 subscribers on the same topic. The observer must see the publisher and both // subscribers with the right kind, primaryIdentifier (== topic), mediaType and labels. -// (In this matched scenario the events fire whether or not they are gated on a connection, so -// this stays green across the follow-up; the semantic then shifts from "created" to "connected".) TEST_F(ITest_CapiServiceDiscovery, observer_sees_publisher_and_both_subscribers) { const std::string topic{"T"}; @@ -285,8 +282,8 @@ TEST_F(ITest_CapiServiceDiscovery, both_subscribers_receive_published_data) EXPECT_TRUE(latch.Wait(2, kWaitTimeout)) << "both subscribers should receive the published sample"; } -// A subscriber with no matching publisher is reported immediately on creation with -// numberOfConnections == 0. The sentinel ensures in-order delivery (see below). +// A subscriber with no matching publisher is reported immediately on creation (as its own kind, with +// no link). The sentinel ensures in-order delivery (see below). // // The barrier is deterministic: the unmatched subscriber and a sentinel publisher are created, in // that order, on the SAME participant. All announcements from one participant reach the observer @@ -319,15 +316,16 @@ TEST_F(ITest_CapiServiceDiscovery, subscriber_visible_immediately_without_matchi 1u) << "a subscriber must be reported immediately on creation, even without a matching publisher"; - // And the connection count must be zero (no publisher matched). - const auto ev = FindEvent(SilKit_Experimental_ServiceKind_DataSubscriber, - SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, lonelyTopic); - EXPECT_EQ(ev.numberOfConnections, 0u); + // And no link exists for that topic (no publisher matched). + EXPECT_EQ(Count(SilKit_Experimental_ServiceKind_Link, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, lonelyTopic), + 0u); } -// When the publisher leaves, the observer must receive a Service_Updated event on the subscriber -// with numberOfConnections == 0. ServiceRemoved does NOT fire (the subscriber is still alive). -TEST_F(ITest_CapiServiceDiscovery, subscriber_updated_when_publisher_leaves) +// When a publisher matches a subscriber, a Link ServiceCreated event fires. When the publisher then +// leaves, no Link removal is emitted; instead the publisher's own ServiceRemoved is observed, from +// which the teardown of the link is inferred. +TEST_F(ITest_CapiServiceDiscovery, link_created_and_publisher_removal_observed) { const std::string topic{"T"}; const std::string mediaType{"M"}; @@ -338,31 +336,26 @@ TEST_F(ITest_CapiServiceDiscovery, subscriber_updated_when_publisher_leaves) publisher->CreateDataPublisher("PubCtrl", spec, 1); subscriber->CreateDataSubscriber("SubCtrl", spec, [](IDataSubscriber*, const DataMessageEvent&) {}); - // Wait for the connection to be established before destroying the publisher. + // Wait for the match to be reported as a Link before destroying the publisher. ASSERT_TRUE(WaitFor([&] { - return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { - return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber - && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated - && e.primaryIdentifier == topic && e.numberOfConnections >= 1; - }); - })) << "observer never saw the subscriber connected to the publisher (Service_Updated, connections>=1)"; + return CountUnlocked(SilKit_Experimental_ServiceKind_Link, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic) + >= 1; + })) << "observer never saw the pub/sub match reported as a Link"; publisher.reset(); ASSERT_TRUE(WaitFor([&] { - return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { - return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber - && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated - && e.primaryIdentifier == topic && e.numberOfConnections == 0; - }); - })) << "observer should see Service_Updated(connections=0) when the publisher leaves"; + return CountUnlocked(SilKit_Experimental_ServiceKind_DataPublisher, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, topic) + >= 1; + })) << "observer should see the publisher's ServiceRemoved when it leaves"; } -// When a publisher matches a subscriber, the Service_Updated event carried on the subscriber must -// name the publisher: connectedParticipantName == publisher's participant name and -// connectedServiceName == publisher's controller name. This lets the observer build a confirmed -// connection graph from service discovery data alone. -TEST_F(ITest_CapiServiceDiscovery, pubsub_service_updated_carries_publisher_peer_identity) +// A pub/sub match Link must name both endpoints: participantName/serviceName the subscriber, and +// connectedParticipantName/connectedServiceName the publisher. This lets the observer build a +// confirmed connection graph from service discovery data alone. +TEST_F(ITest_CapiServiceDiscovery, pubsub_match_link_carries_peer_identity) { const std::string topic{"Sensor"}; const std::string mediaType{"application/octet-stream"}; @@ -375,26 +368,26 @@ TEST_F(ITest_CapiServiceDiscovery, pubsub_service_updated_carries_publisher_peer ASSERT_TRUE(WaitFor([&] { return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { - return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber - && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated - && e.primaryIdentifier == topic && e.numberOfConnections >= 1; + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == topic; }); - })) << "Service_Updated for matched subscriber never arrived"; + })) << "the pub/sub match Link never arrived"; const auto ev = FindEventWhere([&](const ObservedEvent& e) { - return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber - && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated - && e.primaryIdentifier == topic && e.numberOfConnections >= 1; + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == topic; }); - EXPECT_EQ(ev.connectedParticipantName, "PublisherParticipant") - << "subscriber's Service_Updated must identify the publisher's participant"; - EXPECT_EQ(ev.connectedServiceName, "PublisherController") - << "subscriber's Service_Updated must identify the publisher's controller name"; + EXPECT_EQ(ev.participantName, "SubscriberParticipant") << "Link must name the subscriber as the receiving side"; + EXPECT_EQ(ev.serviceName, "SubscriberController"); + EXPECT_EQ(ev.connectedParticipantName, "PublisherParticipant") << "Link must identify the publisher's participant"; + EXPECT_EQ(ev.connectedServiceName, "PublisherController") << "Link must identify the publisher's controller name"; } -// The RPC server's Service_Updated must carry the matching client's participant name and -// controller name, letting the observer resolve concrete RPC call edges. -TEST_F(ITest_CapiServiceDiscovery, rpc_service_updated_carries_client_peer_identity) +// An RPC match Link must carry the matching client's participant name and controller name, letting +// the observer resolve concrete RPC call edges. +TEST_F(ITest_CapiServiceDiscovery, rpc_match_link_carries_peer_identity) { const std::string functionName{"RemoteProc"}; const std::string mediaType{"application/octet-stream"}; @@ -407,27 +400,27 @@ TEST_F(ITest_CapiServiceDiscovery, rpc_service_updated_carries_client_peer_ident ASSERT_TRUE(WaitFor([&] { return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { - return e.serviceKind == SilKit_Experimental_ServiceKind_RpcServer - && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated - && e.primaryIdentifier == functionName && e.numberOfConnections >= 1; + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == functionName; }); - })) << "Service_Updated for matched RPC server never arrived"; + })) << "the RPC match Link never arrived"; const auto ev = FindEventWhere([&](const ObservedEvent& e) { - return e.serviceKind == SilKit_Experimental_ServiceKind_RpcServer - && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated - && e.primaryIdentifier == functionName && e.numberOfConnections >= 1; + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == functionName; }); - EXPECT_EQ(ev.connectedParticipantName, "ClientParticipant") - << "server's Service_Updated must identify the RPC client's participant"; - EXPECT_EQ(ev.connectedServiceName, "ClientController") - << "server's Service_Updated must identify the RPC client's controller name"; + EXPECT_EQ(ev.participantName, "ServerParticipant") << "Link must name the server as the receiving side"; + EXPECT_EQ(ev.serviceName, "ServerController"); + EXPECT_EQ(ev.connectedParticipantName, "ClientParticipant") << "Link must identify the RPC client's participant"; + EXPECT_EQ(ev.connectedServiceName, "ClientController") << "Link must identify the RPC client's controller name"; } -// Attaching an observer to an already-running simulation must recover not only the connection count -// but also the peer identity of connections that existed before the observer registered. This is the -// replay-ordering case: the DataSubscriberInternal may be replayed before its parent/publisher. -TEST_F(ITest_CapiServiceDiscovery, late_observer_recovers_peer_of_preexisting_connection) +// Attaching an observer to an already-running simulation must recover the match, including the peer +// identity, of connections that existed before the observer registered. This is the replay-ordering +// case: the DataSubscriberInternal may be replayed before its parent/publisher. +TEST_F(ITest_CapiServiceDiscovery, late_observer_recovers_preexisting_link) { const std::string topic{"LateTopic"}; const std::string mediaType{"M"}; @@ -440,12 +433,10 @@ TEST_F(ITest_CapiServiceDiscovery, late_observer_recovers_peer_of_preexisting_co // Ensure the match is established (observed via the SetUp observer) before attaching a new one. ASSERT_TRUE(WaitFor([&] { - return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { - return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber - && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated - && e.primaryIdentifier == topic && e.numberOfConnections >= 1; - }); - })) << "the pub/sub connection was never established"; + return CountUnlocked(SilKit_Experimental_ServiceKind_Link, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic) + >= 1; + })) << "the pub/sub link was never established"; // Attach a brand-new observer that must replay the already-running simulation. SilKit_ParticipantConfiguration* config{nullptr}; @@ -465,14 +456,14 @@ TEST_F(ITest_CapiServiceDiscovery, late_observer_recovers_peer_of_preexisting_co std::unique_lock lock{lateCtx.mutex}; const bool recovered = lateCtx.cv.wait_for(lock, kWaitTimeout, [&] { return std::any_of(lateCtx.events.begin(), lateCtx.events.end(), [&](const auto& e) { - return e.serviceKind == SilKit_Experimental_ServiceKind_DataSubscriber - && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated - && e.primaryIdentifier == topic && e.numberOfConnections >= 1 - && e.connectedParticipantName == "LatePub" && e.connectedServiceName == "LatePubCtrl"; + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == topic && e.connectedParticipantName == "LatePub" + && e.connectedServiceName == "LatePubCtrl"; }); }); lock.unlock(); - EXPECT_TRUE(recovered) << "late observer must recover the peer identity of the pre-existing connection"; + EXPECT_TRUE(recovered) << "late observer must recover the pre-existing link and its peer identity"; SilKit_Participant_Destroy(lateObserver); } diff --git a/SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp b/SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp index 503a5e12f..c5fe2502a 100644 --- a/SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp +++ b/SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp @@ -2,15 +2,15 @@ // // SPDX-License-Identifier: MIT -// Integration test: verify that the service-discovery C API correctly reports bus controllers as -// simulated (isSimulated=true) when a network simulator is present, without embedding this -// plumbing into the CAN/Ethernet/LIN/FlexRay routing tests. +// Integration test: verify that the service-discovery C API reports a network simulator as a Link +// service (SilKit_Experimental_ServiceKind_Link) whose primaryIdentifier is the simulated network +// name and whose participantName is the simulating participant. A consumer joins that network name +// against a bus controller's primaryIdentifier to learn the controller is simulated -- without any +// dedicated cross-referencing being done inside the API. // // The test sets up the minimal scenario: one network simulator participant that takes over one CAN // network ("CAN1"), one regular participant with a CAN controller on that network, and one -// autonomous service-discovery observer. After the simulation, the observer must have received at -// least one event for a CAN controller with isSimulated=true and the correct -// simulatingParticipantName. +// autonomous service-discovery observer. #include #include @@ -76,9 +76,8 @@ struct DiscoveryEvent { SilKit_Experimental_ServiceKind serviceKind{}; SilKit_Experimental_ServiceDiscoveryEvent_Type eventType{}; - std::string primaryIdentifier; // networkName for bus controllers - bool isSimulated{false}; - std::string simulatingParticipantName; + std::string participantName; + std::string primaryIdentifier; // networkName for bus controllers and network-simulator links }; struct DiscoveryCtx @@ -96,9 +95,8 @@ void SilKitCALL OnNetSimDiscovery(void* context, SilKit_Experimental_ServiceDisc DiscoveryEvent ev{}; ev.serviceKind = d->serviceKind; ev.eventType = eventType; + ev.participantName = d->participantName ? d->participantName : ""; ev.primaryIdentifier = d->primaryIdentifier ? d->primaryIdentifier : ""; - ev.isSimulated = d->isSimulated != SilKit_False; - ev.simulatingParticipantName = d->simulatingParticipantName ? d->simulatingParticipantName : ""; { std::lock_guard lk{ctx->mutex}; @@ -115,12 +113,11 @@ class ITest_NetSimServiceDiscovery : public ITest_SimTestHarness { }; -// A network simulator takes over the "CAN1" network. A regular participant creates a CAN -// controller on that network. The service-discovery observer must see the CAN controller event -// with isSimulated=true and simulatingParticipantName equal to the network simulator's -// participant name — demonstrating that the new API can identify a network simulator without -// any dedicated "NetworkLink" kind or separate cross-referencing by the caller. -TEST_F(ITest_NetSimServiceDiscovery, can_controller_reported_as_simulated_when_netsim_present) +// A network simulator takes over the "CAN1" network. A regular participant creates a CAN controller +// on that network. The service-discovery observer must see a Link service whose primaryIdentifier is +// "CAN1" and whose participantName is the network simulator's participant name — demonstrating that +// the API surfaces a network simulator as a Link, joinable to bus controllers by network name. +TEST_F(ITest_NetSimServiceDiscovery, netsim_reported_as_link_service) { const std::string netSimName = "NetworkSimulator"; const std::string canPartName = "CanParticipant"; @@ -173,24 +170,28 @@ TEST_F(ITest_NetSimServiceDiscovery, can_controller_reported_as_simulated_when_n auto ok = _simTestHarness->Run(10s); ASSERT_TRUE(ok) << "simulation should complete without timeout"; - // The isSimulated=true events for the CAN controller fire during the simulation (when the - // Link service from the network simulator arrives). They are accumulated in ctx.events by - // the observer's IO thread. Allow a brief settle window after Run() returns for any - // in-flight IO delivery. + // The Link service for the network simulator fires during the simulation. It is accumulated in + // ctx.events by the observer's IO thread. Allow a brief settle window after Run() returns for + // any in-flight IO delivery. { std::unique_lock lk{ctx.mutex}; const bool found = ctx.cv.wait_for(lk, 5s, [&] { - return std::any_of(ctx.events.begin(), ctx.events.end(), [](const DiscoveryEvent& e) { - return e.serviceKind == SilKit_Experimental_ServiceKind_CanController && e.isSimulated; + return std::any_of(ctx.events.begin(), ctx.events.end(), [&](const DiscoveryEvent& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == networkName && e.participantName == netSimName; }); }); - ASSERT_TRUE(found) << "no CAN controller event with isSimulated=true was observed"; - - const auto it = std::find_if(ctx.events.begin(), ctx.events.end(), [](const DiscoveryEvent& e) { - return e.serviceKind == SilKit_Experimental_ServiceKind_CanController && e.isSimulated; + ASSERT_TRUE(found) + << "no Link service with the network name and the network simulator participant was observed"; + + // The CAN controller is reported as its own kind on the same network; joining on + // primaryIdentifier lets a consumer conclude the controller is simulated. + const bool sawController = std::any_of(ctx.events.begin(), ctx.events.end(), [&](const DiscoveryEvent& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_CanController + && e.primaryIdentifier == networkName; }); - EXPECT_EQ(it->simulatingParticipantName, netSimName) - << "simulatingParticipantName must be the network simulator's participant name"; + EXPECT_TRUE(sawController) << "the CAN controller on the simulated network was not observed"; } SilKit_Participant_Destroy(observer); diff --git a/SilKit/include/silkit/capi/Experimental.h b/SilKit/include/silkit/capi/Experimental.h index 9dfb19240..b00cbf4f0 100644 --- a/SilKit/include/silkit/capi/Experimental.h +++ b/SilKit/include/silkit/capi/Experimental.h @@ -37,9 +37,6 @@ typedef uint32_t SilKit_Experimental_ServiceDiscoveryEvent_Type; /*! \brief A service has been removed. */ #define SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved \ ((SilKit_Experimental_ServiceDiscoveryEvent_Type)2) -/*! \brief A property of an existing service has changed (e.g. connection count, simulation status). */ -#define SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated \ - ((SilKit_Experimental_ServiceDiscoveryEvent_Type)3) /*! \brief The kind of a discovered service. Only user-facing services are reported. */ typedef uint32_t SilKit_Experimental_ServiceKind; @@ -52,36 +49,46 @@ typedef uint32_t SilKit_Experimental_ServiceKind; #define SilKit_Experimental_ServiceKind_DataSubscriber ((SilKit_Experimental_ServiceKind)6) #define SilKit_Experimental_ServiceKind_RpcClient ((SilKit_Experimental_ServiceKind)7) #define SilKit_Experimental_ServiceKind_RpcServer ((SilKit_Experimental_ServiceKind)8) +/*! \brief A link between two services: a pub/sub or RPC match, or a network-simulator link. */ +#define SilKit_Experimental_ServiceKind_Link ((SilKit_Experimental_ServiceKind)9) /*! \brief Describes a single discovered service, passed by value to a service discovery handler. * * All pointer members are borrowed and only valid for the duration of the handler invocation. Copy * any data that must outlive the call. * - * Fields \p connectedParticipantName and \p connectedServiceName are populated only in - * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated events that represent a - * connection change (pub/sub match or RPC call). They identify the single peer whose connection - * was added or removed, while \p numberOfConnections gives the new running total. For all other - * event types and all other service kinds these fields are empty strings. + * A \ref SilKit_Experimental_ServiceKind_Link describes a link between two services: a pub/sub or + * RPC match, or a network-simulator link. For a pub/sub or RPC match \p participantName / + * \p serviceName name the receiving side (the DataSubscriber or RpcServer) and + * \p connectedParticipantName / \p connectedServiceName name the peer (the DataPublisher or + * RpcClient); \p primaryIdentifier is the topic or function name. For a network-simulator link + * \p participantName is the simulating participant, \p primaryIdentifier is the simulated network + * name (which matches the \p primaryIdentifier of the affected bus controllers), and the + * \p connected... fields are empty. * - * Fields \p isSimulated and \p simulatingParticipantName are set on bus controllers (CAN, Ethernet, - * FlexRay, LIN) when a network simulator claims ownership of the controller's network. Both a - * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated with \p isSimulated = true - * (simulator already active) and a subsequent - * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated are possible. + * Links are reported as \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated. + * Network-simulator links are additionally reported as + * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved when the simulator detaches. + * A pub/sub or RPC match link does not emit a removal event: its teardown always coincides with a + * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved of one of its endpoints, from + * which the disappearance of the link can be inferred. + * + * The \p connectedParticipantName and \p connectedServiceName fields are empty strings for all + * kinds other than pub/sub and RPC \p SilKit_Experimental_ServiceKind_Link events. */ typedef struct { SilKit_StructHeader structHeader; - //! Name of the participant providing the service. + //! Name of the participant providing the service. For a Link this is the receiving side + //! (subscriber/server) or, for a network-simulator link, the simulating participant. const char* participantName; //! Name of the service (the controller / publisher / subscriber / client / server name). const char* serviceName; //! The kind of service. SilKit_Experimental_ServiceKind serviceKind; - //! The primary, user-facing identifier of the service: the network name for bus controllers, - //! the topic for pub/sub, and the function name for RPC. Suitable as a display / join key for - //! visualization and tooling. + //! The primary, user-facing identifier of the service: the network name for bus controllers and + //! network-simulator links, the topic for pub/sub, and the function name for RPC. Suitable as a + //! display / join key for visualization and tooling. const char* primaryIdentifier; //! Media type for pub/sub and RPC services; empty string when not applicable. const char* mediaType; @@ -89,18 +96,10 @@ typedef struct SilKit_LabelList labelList; //! Reserved for future system-level simulation detection. Currently always an empty string. const char* simulationName; - //! Name of the peer participant; populated only in ServiceUpdated connection events. + //! Name of the peer participant; populated only in pub/sub and RPC Link events. const char* connectedParticipantName; - //! Name of the peer service; populated only in ServiceUpdated connection events. + //! Name of the peer service; populated only in pub/sub and RPC Link events. const char* connectedServiceName; - //! Name of the participant simulating this controller's network; empty when not simulated. - const char* simulatingParticipantName; - //! Number of active matched connections. Reported on the receiving side only: DataSubscribers count - //! matched publishers and RpcServers count matched clients. Always 0 for DataPublishers, RpcClients - //! and bus controllers. - uint32_t numberOfConnections; - //! True when a network simulator owns this bus controller's network. - SilKit_Bool isSimulated; } SilKit_Experimental_ServiceDescriptor; /*! \brief Handler invoked when a user-facing service is created, updated, or removed in the simulation. @@ -144,10 +143,9 @@ typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDiscovery_Creat * * Upon registration the handler is immediately invoked once for every service that is already known, * each reported as \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated. It is - * subsequently invoked for every user-facing service created, updated, or removed. - * Infrastructure/internal services are not reported. Network link events are not reported directly; - * instead, affected bus controllers receive a - * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated with \p isSimulated set. + * subsequently invoked for every user-facing service created or removed. Infrastructure/internal + * services are not reported. Pub/sub and RPC matches as well as network-simulator links are reported + * as \ref SilKit_Experimental_ServiceKind_Link services (see \ref SilKit_Experimental_ServiceDescriptor). * * \note Each call registers an additional, independent handler; handlers cannot be removed and remain * registered for the lifetime of the participant. To observe with a single handler, call this diff --git a/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp b/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp index 06b65e1d4..65db933eb 100644 --- a/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp +++ b/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp @@ -111,9 +111,6 @@ void ServiceDiscovery::SetServiceDiscoveryHandler( serviceDescriptor.simulationName = orEmpty(cServiceDescriptor->simulationName); serviceDescriptor.connectedParticipantName = orEmpty(cServiceDescriptor->connectedParticipantName); serviceDescriptor.connectedServiceName = orEmpty(cServiceDescriptor->connectedServiceName); - serviceDescriptor.simulatingParticipantName = orEmpty(cServiceDescriptor->simulatingParticipantName); - serviceDescriptor.numberOfConnections = cServiceDescriptor->numberOfConnections; - serviceDescriptor.isSimulated = cServiceDescriptor->isSimulated != SilKit_False; auto& userHandler = *static_cast(context); if (userHandler) diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp index d7d93bd30..2469382f9 100644 --- a/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp +++ b/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp @@ -24,8 +24,6 @@ enum class ServiceDiscoveryEventType : SilKit_Experimental_ServiceDiscoveryEvent ServiceCreated = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, //! A service has been removed. ServiceRemoved = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, - //! A property of an existing service has changed (e.g. connection count, simulation status). - ServiceUpdated = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated, }; //! \brief The kind of a discovered service. Only user-facing services are reported. @@ -40,9 +38,19 @@ enum class ServiceKind : SilKit_Experimental_ServiceKind DataSubscriber = SilKit_Experimental_ServiceKind_DataSubscriber, RpcClient = SilKit_Experimental_ServiceKind_RpcClient, RpcServer = SilKit_Experimental_ServiceKind_RpcServer, + //! A link between two services: a pub/sub or RPC match, or a network-simulator link. + Link = SilKit_Experimental_ServiceKind_Link, }; //! \brief Describes a single discovered service, passed to a \ref ServiceDiscoveryHandler. +//! +//! A \ref ServiceKind::Link describes a link between two services: a pub/sub or RPC match, or a +//! network-simulator link. For a pub/sub or RPC match \p participantName / \p serviceName name the +//! receiving side (the DataSubscriber or RpcServer) and \p connectedParticipantName / +//! \p connectedServiceName name the peer (the DataPublisher or RpcClient). For a network-simulator +//! link \p participantName is the simulating participant, \p primaryIdentifier is the simulated +//! network name (matching the affected bus controllers' \p primaryIdentifier), and the +//! \p connected... fields are empty. struct ServiceDescriptor { //! Name of the participant providing the service. @@ -51,8 +59,8 @@ struct ServiceDescriptor std::string serviceName; //! The kind of service. ServiceKind serviceKind{ServiceKind::Undefined}; - //! The primary, user-facing identifier of the service: the network name for bus controllers, - //! the topic for pub/sub, and the function name for RPC. + //! The primary, user-facing identifier of the service: the network name for bus controllers and + //! network-simulator links, the topic for pub/sub, and the function name for RPC. std::string primaryIdentifier; //! Media type for pub/sub and RPC services; empty string when not applicable. std::string mediaType; @@ -60,21 +68,15 @@ struct ServiceDescriptor std::vector labels; //! The simulation name this service belongs to. Empty string when not available. std::string simulationName; - //! Name of the peer participant; populated only in ServiceUpdated connection events. + //! Name of the peer participant; populated only in pub/sub and RPC Link events. std::string connectedParticipantName; - //! Name of the peer service; populated only in ServiceUpdated connection events. + //! Name of the peer service; populated only in pub/sub and RPC Link events. std::string connectedServiceName; - //! Name of the participant simulating this controller's network; empty when not simulated. - std::string simulatingParticipantName; - //! Number of active matched connections (pub/sub or RPC); 0 for bus controllers. - uint32_t numberOfConnections{0}; - //! True when a network simulator owns this bus controller's network. - bool isSimulated{false}; }; -/*! \brief Handler invoked when a user-facing service is created, updated, or removed in the simulation. +/*! \brief Handler invoked when a user-facing service is created or removed in the simulation. * - * \param eventType Whether the service was created, updated, or removed. + * \param eventType Whether the service was created or removed. * \param serviceDescriptor The affected service. */ using ServiceDiscoveryHandler = diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp index 5915b0f2e..2bf66ba58 100644 --- a/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp +++ b/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp @@ -46,6 +46,8 @@ std::string to_string(ServiceKind serviceKind) return "RpcClient"; case ServiceKind::RpcServer: return "RpcServer"; + case ServiceKind::Link: + return "Link"; } std::stringstream out; @@ -63,8 +65,6 @@ std::string to_string(ServiceDiscoveryEventType eventType) return "ServiceCreated"; case ServiceDiscoveryEventType::ServiceRemoved: return "ServiceRemoved"; - case ServiceDiscoveryEventType::ServiceUpdated: - return "ServiceUpdated"; } std::stringstream out; diff --git a/SilKit/source/capi/CMakeLists.txt b/SilKit/source/capi/CMakeLists.txt index 046a14513..8daf12ed6 100644 --- a/SilKit/source/capi/CMakeLists.txt +++ b/SilKit/source/capi/CMakeLists.txt @@ -19,6 +19,8 @@ add_library(O_SilKit_Capi OBJECT CapiVendor.cpp CapiVersion.cpp CapiNetworkSimulator.cpp + ServiceObserver.cpp + ServiceObserver.hpp TypeConversion.hpp ) @@ -32,6 +34,7 @@ add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiEthernet.cpp LIBS add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiFlexray.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiData.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiServiceDiscovery.cpp LIBS S_SilKitImpl I_SilKit) +add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_ServiceObserver.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiRpc.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiLogger.cpp LIBS S_SilKitImpl) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiVersion.cpp LIBS S_SilKitImpl) diff --git a/SilKit/source/capi/CapiExperimental.cpp b/SilKit/source/capi/CapiExperimental.cpp index 8d2517c0f..f44c3a2ee 100644 --- a/SilKit/source/capi/CapiExperimental.cpp +++ b/SilKit/source/capi/CapiExperimental.cpp @@ -5,24 +5,15 @@ #include "silkit/capi/SilKit.h" #include "silkit/SilKit.hpp" #include "silkit/participant/exception.hpp" -#include "silkit/services/datatypes.hpp" #include "capi/CapiImpl.hpp" +#include "capi/ServiceObserver.hpp" #include "core/internal/IParticipantInternal.hpp" #include "core/service/IServiceDiscovery.hpp" #include "core/internal/ServiceDescriptor.hpp" -#include "core/internal/ServiceConfigKeys.hpp" -#include "config/YamlParser.hpp" - -#include #include -#include -#include -#include -#include -#include namespace { @@ -39,743 +30,6 @@ auto GetServiceDiscovery(SilKit_Participant* participant) -> Discovery::IService return participantInternal->GetServiceDiscovery(); } -auto ToC(Discovery::ServiceDiscoveryEvent::Type type) -> SilKit_Experimental_ServiceDiscoveryEvent_Type -{ - using Type = Discovery::ServiceDiscoveryEvent::Type; - switch (type) - { - case Type::ServiceCreated: - return SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; - case Type::ServiceRemoved: - return SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; - default: - return SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid; - } -} - -// Owns the backing storage for the label list so that the borrowed c-string pointers in the -// SilKit_Experimental_ServiceDescriptor remain valid for the duration of the handler invocation. -struct LabelStorage -{ - std::vector labels; - std::vector cLabels; -}; - -// Maps the internal ServiceDescriptor to the public struct. Returns false if the service is not user-facing -// (infrastructure / internal services / network links), in which case the handler must not be invoked. -// Connection-specific and simulation-specific fields (numberOfConnections, connectedParticipantName, -// connectedServiceName, isSimulated, simulatingParticipantName) are initialised to safe empty values -// here; callers set them from PendingEmission after this function returns. -auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, - SilKit_Experimental_ServiceDescriptor& out, LabelStorage& storage) -> bool -{ - const auto& supplementalData = serviceDescriptor.GetSupplementalDataRef(); - - const auto findValue = [&supplementalData](const std::string& key) -> const char* { - const auto it = supplementalData.find(key); - return it == supplementalData.end() ? nullptr : it->second.c_str(); - }; - - SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, out); - out.participantName = serviceDescriptor.GetParticipantName().c_str(); - out.serviceName = serviceDescriptor.GetServiceName().c_str(); - out.primaryIdentifier = serviceDescriptor.GetNetworkName().c_str(); - out.mediaType = ""; - out.simulationName = serviceDescriptor.GetSimulationName().c_str(); - out.connectedParticipantName = ""; - out.connectedServiceName = ""; - out.simulatingParticipantName = ""; - // numberOfConnections = 0, isSimulated = SilKit_False (from memset above) - - // Network links are handled internally (fan-out to bus controllers) and are not exposed directly. - if (serviceDescriptor.GetServiceType() == SilKit::Core::ServiceType::Link) - { - return false; - } - - std::string controllerType; - if (!serviceDescriptor.GetSupplementalDataItem(Discovery::controllerType, controllerType)) - { - return false; - } - - // Decode the YAML-encoded matching labels into the public label list. A malformed value must not propagate - // an exception: the service is still reported, just without labels. - const auto decodeLabels = [&](const std::string& labelsKey) { - const char* labelsStr = findValue(labelsKey); - if (labelsStr == nullptr || *labelsStr == '\0') - { - return; - } - try - { - storage.labels = SilKit::Config::Deserialize>(labelsStr); - } - catch (...) - { - return; - } - storage.cLabels.reserve(storage.labels.size()); - for (const auto& label : storage.labels) - { - SilKit_Label cLabel; - cLabel.key = label.key.c_str(); - cLabel.value = label.value.c_str(); - cLabel.kind = static_cast(label.kind); - storage.cLabels.push_back(cLabel); - } - out.labelList.numLabels = storage.cLabels.size(); - out.labelList.labels = storage.cLabels.data(); - }; - - if (controllerType == Discovery::controllerTypeCan) - { - out.serviceKind = SilKit_Experimental_ServiceKind_CanController; - } - else if (controllerType == Discovery::controllerTypeEthernet) - { - out.serviceKind = SilKit_Experimental_ServiceKind_EthernetController; - } - else if (controllerType == Discovery::controllerTypeFlexray) - { - out.serviceKind = SilKit_Experimental_ServiceKind_FlexrayController; - } - else if (controllerType == Discovery::controllerTypeLin) - { - out.serviceKind = SilKit_Experimental_ServiceKind_LinController; - } - else if (controllerType == Discovery::controllerTypeDataPublisher) - { - out.serviceKind = SilKit_Experimental_ServiceKind_DataPublisher; - if (const char* topic = findValue(Discovery::supplKeyDataPublisherTopic)) - { - out.primaryIdentifier = topic; - } - if (const char* mediaType = findValue(Discovery::supplKeyDataPublisherMediaType)) - { - out.mediaType = mediaType; - } - decodeLabels(Discovery::supplKeyDataPublisherPubLabels); - } - else if (controllerType == Discovery::controllerTypeDataSubscriber) - { - out.serviceKind = SilKit_Experimental_ServiceKind_DataSubscriber; - if (const char* topic = findValue(Discovery::supplKeyDataSubscriberTopic)) - { - out.primaryIdentifier = topic; - } - if (const char* mediaType = findValue(Discovery::supplKeyDataSubscriberMediaType)) - { - out.mediaType = mediaType; - } - decodeLabels(Discovery::supplKeyDataSubscriberSubLabels); - } - else if (controllerType == Discovery::controllerTypeRpcClient) - { - out.serviceKind = SilKit_Experimental_ServiceKind_RpcClient; - if (const char* functionName = findValue(Discovery::supplKeyRpcClientFunctionName)) - { - out.primaryIdentifier = functionName; - } - if (const char* mediaType = findValue(Discovery::supplKeyRpcClientMediaType)) - { - out.mediaType = mediaType; - } - decodeLabels(Discovery::supplKeyRpcClientLabels); - } - else if (controllerType == Discovery::controllerTypeRpcServer) - { - out.serviceKind = SilKit_Experimental_ServiceKind_RpcServer; - if (const char* functionName = findValue(Discovery::supplKeyRpcServerFunctionName)) - { - out.primaryIdentifier = functionName; - } - if (const char* mediaType = findValue(Discovery::supplKeyRpcServerMediaType)) - { - out.mediaType = mediaType; - } - decodeLabels(Discovery::supplKeyRpcServerLabels); - } - else - { - // Infrastructure / internal controllers (ServiceDiscovery, SystemMonitor, lifecycle, metrics, - // DataSubscriberInternal, RpcServerInternal, ...) are not user-facing and are not reported. - return false; - } - - return true; -} - -// Identifies a service across the simulation: (participant name, service id). -using ServiceKey = std::pair; -// Identifies a network: (network name, network type). Used to correlate bus controllers with links. -using NetworkKey = std::pair; - -// Peer identity for pub/sub and RPC connection resolution. -struct PeerInfo -{ - std::string participantName; - std::string serviceName; -}; - -// Tracks a user-facing service (DataSubscriber or RpcServer) for connection counting and peer reveal. -// connKeys holds the internal-connection endpoints attached to this parent; its size is the connection -// count. Endpoints may be recorded before the parent's own descriptor is known (out-of-order replay), -// in which case haveDescriptor is false until the parent is discovered. -struct TrackedEntry -{ - SilKit::Core::ServiceDescriptor descriptor; - bool haveDescriptor{false}; - std::set connKeys; -}; - -// A confirmed connection endpoint (DataSubscriberInternal / RpcServerInternal): links the internal -// endpoint to its parent (user-facing subscriber/server) and remembers the peer's UUID (the -// DataPublisher / RpcClient networkName) so the peer identity can be resolved, possibly later. -struct ConnInfo -{ - ServiceKey parentKey; - std::string peerUuid; -}; - -// A synthesized event, queued while the state lock is held and dispatched after it is released. -// All std::string members hold backing storage for the pointer fields set in the emitted struct. -struct PendingEmission -{ - SilKit_Experimental_ServiceDiscoveryEvent_Type type{SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid}; - SilKit::Core::ServiceDescriptor descriptor; - uint32_t numberOfConnections{0}; - std::string connectedParticipantName; - std::string connectedServiceName; - bool isSimulated{false}; - std::string simulatingParticipantName; -}; - -// Per-observer bookkeeping. Lives as long as the participant's service discovery, because it is -// captured by the registered handler (which is never unregistered). -struct ObserverState -{ - SilKit_Experimental_ServiceDiscoveryHandler_t handler{}; - void* context{nullptr}; - - std::mutex mutex; - - // DataSubscribers and RpcServers tracked for connection counting and peer resolution. - std::map tracked; - // Internal-connection endpoint key -> connection info (parent key + peer UUID). - std::map connections; - // Peer UUID -> connection endpoints whose peer identity is not yet known, awaiting the peer's own - // discovery. Enables revealing the peer of a connection seen before its DataPublisher / RpcClient. - std::map> pendingByPeerUuid; - // DataPublisher / RpcClient UUID (= their networkName) -> peer identity. Keyed by globally unique - // UUID, so publishers and clients share this map without collision. - std::map peersByUuid; - - // Bus controllers by ServiceKey -> stored descriptor, for link-event fan-out. - std::map busControllers; - // NetworkKey -> set of bus controller ServiceKeys (reverse lookup for link events). - std::map> controllersByNetwork; - - // Active network simulators: NetworkKey -> simulator's participant name. - std::map activeLinks; -}; - -void EmitPending(ObserverState& state, const PendingEmission& e) -{ - SilKit_Experimental_ServiceDescriptor out{}; - LabelStorage storage; - if (!ClassifyAndFill(e.descriptor, out, storage)) - { - return; - } - out.numberOfConnections = e.numberOfConnections; - out.connectedParticipantName = e.connectedParticipantName.c_str(); - out.connectedServiceName = e.connectedServiceName.c_str(); - out.isSimulated = e.isSimulated ? SilKit_True : SilKit_False; - out.simulatingParticipantName = e.simulatingParticipantName.c_str(); - state.handler(state.context, e.type, &out); -} - -// ---- Connection bookkeeping helpers. All are called with state.mutex held and append synthesized -// ServiceUpdated emissions for the *parent* service (subscriber/server); the emissions are always -// dispatched by the caller after the lock is released. Peer identity is filled in when known, and -// revealed via a later ServiceUpdated once the peer (publisher/client) is discovered. This keeps -// the connection graph reconstructable regardless of discovery/replay ordering. ---- - -auto MakeParentUpdate(const TrackedEntry& entry, const PeerInfo* peer) -> PendingEmission -{ - PendingEmission e; - e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; - e.descriptor = entry.descriptor; - e.numberOfConnections = static_cast(entry.connKeys.size()); - if (peer != nullptr) - { - e.connectedParticipantName = peer->participantName; - e.connectedServiceName = peer->serviceName; - } - return e; -} - -// A parent (DataSubscriber / RpcServer) was created: emit its ServiceCreated and reveal the peers of -// any connections already recorded against it (possible when discovery replays out of order). -void OnParentCreated(ObserverState& state, const ServiceKey& parentKey, - const SilKit::Core::ServiceDescriptor& descriptor, std::vector& emissions) -{ - auto& entry = state.tracked[parentKey]; - entry.descriptor = descriptor; - entry.haveDescriptor = true; - - PendingEmission created; - created.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; - created.descriptor = descriptor; - created.numberOfConnections = static_cast(entry.connKeys.size()); - emissions.push_back(std::move(created)); - - for (const auto& connKey : entry.connKeys) - { - const auto cit = state.connections.find(connKey); - if (cit == state.connections.end()) - { - continue; - } - const auto pit = state.peersByUuid.find(cit->second.peerUuid); - if (pit != state.peersByUuid.end()) - { - emissions.push_back(MakeParentUpdate(entry, &pit->second)); - } - else - { - state.pendingByPeerUuid[cit->second.peerUuid].insert(connKey); - } - } -} - -// A parent (DataSubscriber / RpcServer) was removed: drop it and all of its connection bookkeeping. -void OnParentRemoved(ObserverState& state, const ServiceKey& parentKey) -{ - const auto tit = state.tracked.find(parentKey); - if (tit == state.tracked.end()) - { - return; - } - for (const auto& connKey : tit->second.connKeys) - { - const auto cit = state.connections.find(connKey); - if (cit == state.connections.end()) - { - continue; - } - const auto pit = state.pendingByPeerUuid.find(cit->second.peerUuid); - if (pit != state.pendingByPeerUuid.end()) - { - pit->second.erase(connKey); - if (pit->second.empty()) - { - state.pendingByPeerUuid.erase(pit); - } - } - state.connections.erase(cit); - } - state.tracked.erase(tit); -} - -// A confirmed connection endpoint (DataSubscriberInternal / RpcServerInternal) appeared. -void OnConnectionCreated(ObserverState& state, const ServiceKey& connKey, const ServiceKey& parentKey, - const std::string& peerUuid, std::vector& emissions) -{ - if (state.connections.count(connKey) != 0) - { - return; // already counted - } - state.connections[connKey] = ConnInfo{parentKey, peerUuid}; - auto& entry = state.tracked[parentKey]; - entry.connKeys.insert(connKey); - - if (!entry.haveDescriptor) - { - return; // revealed when the parent itself is discovered - } - const auto pit = state.peersByUuid.find(peerUuid); - if (pit != state.peersByUuid.end()) - { - emissions.push_back(MakeParentUpdate(entry, &pit->second)); - } - else - { - // Surface the new connection count now; reveal the peer once its publisher/client is known. - emissions.push_back(MakeParentUpdate(entry, nullptr)); - state.pendingByPeerUuid[peerUuid].insert(connKey); - } -} - -// A confirmed connection endpoint was removed (a match dissolved). -void OnConnectionRemoved(ObserverState& state, const ServiceKey& connKey, std::vector& emissions) -{ - const auto cit = state.connections.find(connKey); - if (cit == state.connections.end()) - { - return; - } - const auto parentKey = cit->second.parentKey; - const auto peerUuid = cit->second.peerUuid; - state.connections.erase(cit); - - const auto pit = state.pendingByPeerUuid.find(peerUuid); - if (pit != state.pendingByPeerUuid.end()) - { - pit->second.erase(connKey); - if (pit->second.empty()) - { - state.pendingByPeerUuid.erase(pit); - } - } - - const auto tit = state.tracked.find(parentKey); - if (tit == state.tracked.end()) - { - return; - } - tit->second.connKeys.erase(connKey); - if (tit->second.haveDescriptor) - { - const auto peerIt = state.peersByUuid.find(peerUuid); - const PeerInfo* peer = peerIt != state.peersByUuid.end() ? &peerIt->second : nullptr; - emissions.push_back(MakeParentUpdate(tit->second, peer)); - } -} - -// A peer (DataPublisher / RpcClient) appeared: remember its identity and reveal it to any connection -// that was recorded before the peer was known. -void OnPeerCreated(ObserverState& state, const std::string& peerUuid, PeerInfo peerInfo, - std::vector& emissions) -{ - state.peersByUuid[peerUuid] = std::move(peerInfo); - const auto pit = state.pendingByPeerUuid.find(peerUuid); - if (pit == state.pendingByPeerUuid.end()) - { - return; - } - const auto& peer = state.peersByUuid[peerUuid]; - for (const auto& connKey : pit->second) - { - const auto cit = state.connections.find(connKey); - if (cit == state.connections.end()) - { - continue; - } - const auto tit = state.tracked.find(cit->second.parentKey); - if (tit != state.tracked.end() && tit->second.haveDescriptor) - { - emissions.push_back(MakeParentUpdate(tit->second, &peer)); - } - } - state.pendingByPeerUuid.erase(pit); -} - -// A peer (DataPublisher / RpcClient) was removed: forget its identity. Existing connection edges have -// already been reported; the connection's own removal (if any) reports the decremented count. -void OnPeerRemoved(ObserverState& state, const std::string& peerUuid) -{ - state.peersByUuid.erase(peerUuid); -} - -// Handles a single internal discovery event, translating it into zero or more public emissions. -// Emissions are always dispatched outside the state lock so user code never runs while we hold it. -void HandleDiscoveryEvent(ObserverState& state, Discovery::ServiceDiscoveryEvent::Type type, - const SilKit::Core::ServiceDescriptor& descriptor) -{ - using EventType = Discovery::ServiceDiscoveryEvent::Type; - - // ---- Network links: track and fan-out isSimulated updates to affected bus controllers ---- - if (descriptor.GetServiceType() == SilKit::Core::ServiceType::Link) - { - const NetworkKey networkKey{descriptor.GetNetworkName(), descriptor.GetNetworkType()}; - std::vector emissions; - { - std::lock_guard lock{state.mutex}; - if (type == EventType::ServiceCreated) - { - state.activeLinks[networkKey] = descriptor.GetParticipantName(); - const auto cit = state.controllersByNetwork.find(networkKey); - if (cit != state.controllersByNetwork.end()) - { - for (const auto& controllerKey : cit->second) - { - PendingEmission e; - e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; - e.descriptor = state.busControllers.at(controllerKey); - e.isSimulated = true; - e.simulatingParticipantName = descriptor.GetParticipantName(); - emissions.push_back(std::move(e)); - } - } - } - else if (type == EventType::ServiceRemoved) - { - state.activeLinks.erase(networkKey); - const auto cit = state.controllersByNetwork.find(networkKey); - if (cit != state.controllersByNetwork.end()) - { - for (const auto& controllerKey : cit->second) - { - PendingEmission e; - e.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated; - e.descriptor = state.busControllers.at(controllerKey); - e.isSimulated = false; - emissions.push_back(std::move(e)); - } - } - } - } - for (const auto& e : emissions) - { - EmitPending(state, e); - } - return; - } - - std::string controllerType; - descriptor.GetSupplementalDataItem(Discovery::controllerType, controllerType); - - // ---- Bus controllers: track for link fan-out; emit immediately with current isSimulated state ---- - if (controllerType == Discovery::controllerTypeCan || controllerType == Discovery::controllerTypeEthernet - || controllerType == Discovery::controllerTypeFlexray || controllerType == Discovery::controllerTypeLin) - { - const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; - const NetworkKey networkKey{descriptor.GetNetworkName(), descriptor.GetNetworkType()}; - PendingEmission emission; - emission.descriptor = descriptor; - { - std::lock_guard lock{state.mutex}; - if (type == EventType::ServiceCreated) - { - state.busControllers[key] = descriptor; - state.controllersByNetwork[networkKey].insert(key); - emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; - const auto linkIt = state.activeLinks.find(networkKey); - if (linkIt != state.activeLinks.end()) - { - emission.isSimulated = true; - emission.simulatingParticipantName = linkIt->second; - } - } - else if (type == EventType::ServiceRemoved) - { - state.busControllers.erase(key); - auto& ctrlSet = state.controllersByNetwork[networkKey]; - ctrlSet.erase(key); - if (ctrlSet.empty()) - { - state.controllersByNetwork.erase(networkKey); - } - emission.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; - } - } - EmitPending(state, emission); - return; - } - - // ---- DataPublisher: a user-facing service AND a connection peer. Emit its own event first, then - // reveal it on any subscriber connection recorded before this publisher was known. ---- - if (controllerType == Discovery::controllerTypeDataPublisher) - { - std::vector reveals; - { - std::lock_guard lock{state.mutex}; - if (type == EventType::ServiceCreated) - { - OnPeerCreated(state, descriptor.GetNetworkName(), - PeerInfo{descriptor.GetParticipantName(), descriptor.GetServiceName()}, reveals); - } - else if (type == EventType::ServiceRemoved) - { - OnPeerRemoved(state, descriptor.GetNetworkName()); - } - } - PendingEmission own; - own.type = ToC(type); - own.descriptor = descriptor; - EmitPending(state, own); - for (const auto& e : reveals) - { - EmitPending(state, e); - } - return; - } - - // ---- DataSubscriber: a tracked parent. OnParentCreated emits ServiceCreated and reveals any - // already-recorded connections; removal drops all of its bookkeeping. ---- - if (controllerType == Discovery::controllerTypeDataSubscriber) - { - const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; - std::vector emissions; - { - std::lock_guard lock{state.mutex}; - if (type == EventType::ServiceCreated) - { - OnParentCreated(state, key, descriptor, emissions); - } - else if (type == EventType::ServiceRemoved) - { - OnParentRemoved(state, key); - PendingEmission removed; - removed.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; - removed.descriptor = descriptor; - emissions.push_back(std::move(removed)); - } - } - for (const auto& e : emissions) - { - EmitPending(state, e); - } - return; - } - - // ---- DataSubscriberInternal: a confirmed pub/sub match; drives Service_Updated on the parent - // subscriber, with the publisher (peerUuid = networkName) as the peer. ---- - if (controllerType == Discovery::controllerTypeDataSubscriberInternal) - { - const ServiceKey internalKey{descriptor.GetParticipantName(), descriptor.GetServiceId()}; - std::string parentIdStr; - if (!descriptor.GetSupplementalDataItem(Discovery::supplKeyDataSubscriberInternalParentServiceID, parentIdStr)) - { - return; - } - SilKit::Core::EndpointId parentServiceId{0}; - try - { - parentServiceId = static_cast(std::stoull(parentIdStr)); - } - catch (...) - { - return; - } - const ServiceKey parentKey{descriptor.GetParticipantName(), parentServiceId}; - const std::string publisherUuid = descriptor.GetNetworkName(); - - std::vector emissions; - { - std::lock_guard lock{state.mutex}; - if (type == EventType::ServiceCreated) - { - OnConnectionCreated(state, internalKey, parentKey, publisherUuid, emissions); - } - else if (type == EventType::ServiceRemoved) - { - OnConnectionRemoved(state, internalKey, emissions); - } - } - for (const auto& e : emissions) - { - EmitPending(state, e); - } - return; - } - - // ---- RpcClient: a user-facing service AND a connection peer. Emit its own event first, then - // reveal it on any server connection recorded before this client was known. ---- - if (controllerType == Discovery::controllerTypeRpcClient) - { - std::vector reveals; - { - std::lock_guard lock{state.mutex}; - if (type == EventType::ServiceCreated) - { - OnPeerCreated(state, descriptor.GetNetworkName(), - PeerInfo{descriptor.GetParticipantName(), descriptor.GetServiceName()}, reveals); - } - else if (type == EventType::ServiceRemoved) - { - OnPeerRemoved(state, descriptor.GetNetworkName()); - } - } - PendingEmission own; - own.type = ToC(type); - own.descriptor = descriptor; - EmitPending(state, own); - for (const auto& e : reveals) - { - EmitPending(state, e); - } - return; - } - - // ---- RpcServer: a tracked parent. OnParentCreated emits ServiceCreated and reveals any - // already-recorded connections; removal drops all of its bookkeeping. ---- - if (controllerType == Discovery::controllerTypeRpcServer) - { - const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; - std::vector emissions; - { - std::lock_guard lock{state.mutex}; - if (type == EventType::ServiceCreated) - { - OnParentCreated(state, key, descriptor, emissions); - } - else if (type == EventType::ServiceRemoved) - { - OnParentRemoved(state, key); - PendingEmission removed; - removed.type = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; - removed.descriptor = descriptor; - emissions.push_back(std::move(removed)); - } - } - for (const auto& e : emissions) - { - EmitPending(state, e); - } - return; - } - - // ---- RpcServerInternal: a confirmed RPC match; emit Service_Updated on the parent server ---- - if (controllerType == Discovery::controllerTypeRpcServerInternal) - { - const ServiceKey internalKey{descriptor.GetParticipantName(), descriptor.GetServiceId()}; - std::string parentIdStr; - if (!descriptor.GetSupplementalDataItem(Discovery::supplKeyRpcServerInternalParentServiceID, parentIdStr)) - { - return; - } - SilKit::Core::EndpointId parentServiceId{0}; - try - { - parentServiceId = static_cast(std::stoull(parentIdStr)); - } - catch (...) - { - return; - } - const ServiceKey parentKey{descriptor.GetParticipantName(), parentServiceId}; - std::string clientUuid; - descriptor.GetSupplementalDataItem(Discovery::supplKeyRpcServerInternalClientUUID, clientUuid); - - std::vector emissions; - { - std::lock_guard lock{state.mutex}; - if (type == EventType::ServiceCreated) - { - OnConnectionCreated(state, internalKey, parentKey, clientUuid, emissions); - } - else if (type == EventType::ServiceRemoved) - { - OnConnectionRemoved(state, internalKey, emissions); - } - } - for (const auto& e : emissions) - { - EmitPending(state, e); - } - return; - } - - // Everything else (infrastructure, system services): forwarded as-is; ClassifyAndFill suppresses them. - PendingEmission e; - e.type = ToC(type); - e.descriptor = descriptor; - EmitPending(state, e); -} - } // namespace @@ -804,19 +58,17 @@ try auto* cppServiceDiscovery = reinterpret_cast(serviceDiscovery); - auto state = std::make_shared(); - state->handler = handler; - state->context = context; + auto observer = std::make_shared(handler, context); cppServiceDiscovery->RegisterServiceDiscoveryHandler( - [state](Discovery::ServiceDiscoveryEvent::Type type, - const SilKit::Core::ServiceDescriptor& serviceDescriptor) { + [observer](Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& serviceDescriptor) { // Invoked by the internal service discovery with its lock held, serialized, on an unspecified // thread (an IO worker for remote events, or the caller's thread for locally created/removed // services). Exceptions must never propagate into SIL Kit internals. try { - HandleDiscoveryEvent(*state, type, serviceDescriptor); + observer->HandleEvent(type, serviceDescriptor); } catch (...) { diff --git a/SilKit/source/capi/ServiceObserver.cpp b/SilKit/source/capi/ServiceObserver.cpp new file mode 100644 index 000000000..90a8d9cd7 --- /dev/null +++ b/SilKit/source/capi/ServiceObserver.cpp @@ -0,0 +1,424 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "capi/ServiceObserver.hpp" + +#include "silkit/services/datatypes.hpp" + +#include "core/internal/ServiceConfigKeys.hpp" + +#include "config/YamlParser.hpp" + +#include + +namespace { + +namespace Discovery = SilKit::Core::Discovery; + +auto ToC(Discovery::ServiceDiscoveryEvent::Type type) -> SilKit_Experimental_ServiceDiscoveryEvent_Type +{ + using Type = Discovery::ServiceDiscoveryEvent::Type; + switch (type) + { + case Type::ServiceCreated: + return SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; + case Type::ServiceRemoved: + return SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; + default: + return SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid; + } +} + +// Owns the backing storage for the label list so that the borrowed c-string pointers in the +// SilKit_Experimental_ServiceDescriptor remain valid for the duration of the handler invocation. +struct LabelStorage +{ + std::vector labels; + std::vector cLabels; +}; + +// Maps the internal ServiceDescriptor to the public struct. Returns false if the service is not user-facing +// (infrastructure / internal endpoints), in which case the handler must not be invoked. The connectedParticipantName +// and connectedServiceName fields are initialised to empty strings here; the link path sets them (and overrides +// serviceKind to Link) after this function returns. +auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, + SilKit_Experimental_ServiceDescriptor& out, LabelStorage& storage) -> bool +{ + const auto& supplementalData = serviceDescriptor.GetSupplementalDataRef(); + + const auto findValue = [&supplementalData](const std::string& key) -> const char* { + const auto it = supplementalData.find(key); + return it == supplementalData.end() ? nullptr : it->second.c_str(); + }; + + SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, out); + out.participantName = serviceDescriptor.GetParticipantName().c_str(); + out.serviceName = serviceDescriptor.GetServiceName().c_str(); + out.primaryIdentifier = serviceDescriptor.GetNetworkName().c_str(); + out.mediaType = ""; + out.simulationName = serviceDescriptor.GetSimulationName().c_str(); + out.connectedParticipantName = ""; + out.connectedServiceName = ""; + + // Network-simulator links are user-facing: they are reported as a Link service whose primaryIdentifier is + // the simulated network name (matching the affected bus controllers' primaryIdentifier). + if (serviceDescriptor.GetServiceType() == SilKit::Core::ServiceType::Link) + { + out.serviceKind = SilKit_Experimental_ServiceKind_Link; + return true; + } + + std::string controllerType; + if (!serviceDescriptor.GetSupplementalDataItem(Discovery::controllerType, controllerType)) + { + return false; + } + + // Decode the YAML-encoded matching labels into the public label list. A malformed value must not propagate + // an exception: the service is still reported, just without labels. + const auto decodeLabels = [&](const std::string& labelsKey) { + const char* labelsStr = findValue(labelsKey); + if (labelsStr == nullptr || *labelsStr == '\0') + { + return; + } + try + { + storage.labels = SilKit::Config::Deserialize>(labelsStr); + } + catch (...) + { + return; + } + storage.cLabels.reserve(storage.labels.size()); + for (const auto& label : storage.labels) + { + SilKit_Label cLabel; + cLabel.key = label.key.c_str(); + cLabel.value = label.value.c_str(); + cLabel.kind = static_cast(label.kind); + storage.cLabels.push_back(cLabel); + } + out.labelList.numLabels = storage.cLabels.size(); + out.labelList.labels = storage.cLabels.data(); + }; + + if (controllerType == Discovery::controllerTypeCan) + { + out.serviceKind = SilKit_Experimental_ServiceKind_CanController; + } + else if (controllerType == Discovery::controllerTypeEthernet) + { + out.serviceKind = SilKit_Experimental_ServiceKind_EthernetController; + } + else if (controllerType == Discovery::controllerTypeFlexray) + { + out.serviceKind = SilKit_Experimental_ServiceKind_FlexrayController; + } + else if (controllerType == Discovery::controllerTypeLin) + { + out.serviceKind = SilKit_Experimental_ServiceKind_LinController; + } + else if (controllerType == Discovery::controllerTypeDataPublisher) + { + out.serviceKind = SilKit_Experimental_ServiceKind_DataPublisher; + if (const char* topic = findValue(Discovery::supplKeyDataPublisherTopic)) + { + out.primaryIdentifier = topic; + } + if (const char* mediaType = findValue(Discovery::supplKeyDataPublisherMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyDataPublisherPubLabels); + } + else if (controllerType == Discovery::controllerTypeDataSubscriber) + { + out.serviceKind = SilKit_Experimental_ServiceKind_DataSubscriber; + if (const char* topic = findValue(Discovery::supplKeyDataSubscriberTopic)) + { + out.primaryIdentifier = topic; + } + if (const char* mediaType = findValue(Discovery::supplKeyDataSubscriberMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyDataSubscriberSubLabels); + } + else if (controllerType == Discovery::controllerTypeRpcClient) + { + out.serviceKind = SilKit_Experimental_ServiceKind_RpcClient; + if (const char* functionName = findValue(Discovery::supplKeyRpcClientFunctionName)) + { + out.primaryIdentifier = functionName; + } + if (const char* mediaType = findValue(Discovery::supplKeyRpcClientMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyRpcClientLabels); + } + else if (controllerType == Discovery::controllerTypeRpcServer) + { + out.serviceKind = SilKit_Experimental_ServiceKind_RpcServer; + if (const char* functionName = findValue(Discovery::supplKeyRpcServerFunctionName)) + { + out.primaryIdentifier = functionName; + } + if (const char* mediaType = findValue(Discovery::supplKeyRpcServerMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyRpcServerLabels); + } + else + { + // Infrastructure / internal controllers (ServiceDiscovery, SystemMonitor, lifecycle, metrics, + // DataSubscriberInternal, RpcServerInternal, ...) are not user-facing and are not reported. + return false; + } + + return true; +} + +auto TryParseEndpointId(const std::string& text, SilKit::Core::EndpointId& out) -> bool +{ + try + { + out = static_cast(std::stoull(text)); + return true; + } + catch (...) + { + return false; + } +} + +} // namespace + + +namespace VSilKit { + +ServiceObserver::ServiceObserver(SilKit_Experimental_ServiceDiscoveryHandler_t handler, void* context) + : _handler{handler} + , _context{context} +{ +} + +auto ServiceObserver::MakeLink(const SilKit::Core::ServiceDescriptor& parent, + const SilKit::Core::ServiceDescriptor& peer) -> LinkEmission +{ + return LinkEmission{parent, peer.GetParticipantName(), peer.GetServiceName()}; +} + +void ServiceObserver::EmitService(SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const SilKit::Core::ServiceDescriptor& descriptor) +{ + SilKit_Experimental_ServiceDescriptor out{}; + LabelStorage storage; + if (!ClassifyAndFill(descriptor, out, storage)) + { + return; + } + _handler(_context, type, &out); +} + +void ServiceObserver::EmitLink(const LinkEmission& emission) +{ + SilKit_Experimental_ServiceDescriptor out{}; + LabelStorage storage; + if (!ClassifyAndFill(emission.parentDescriptor, out, storage)) + { + return; + } + out.serviceKind = SilKit_Experimental_ServiceKind_Link; + out.connectedParticipantName = emission.connectedParticipantName.c_str(); + out.connectedServiceName = emission.connectedServiceName.c_str(); + _handler(_context, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, &out); +} + +void ServiceObserver::DrainResolvablePending(std::vector& emissions) +{ + auto it = _pending.begin(); + while (it != _pending.end()) + { + const auto parentIt = _parents.find(it->parentKey); + const auto peerIt = _peersByUuid.find(it->peerUuid); + if (parentIt != _parents.end() && peerIt != _peersByUuid.end()) + { + emissions.push_back(MakeLink(parentIt->second, peerIt->second)); + it = _pending.erase(it); + } + else + { + ++it; + } + } +} + +// An internal-match endpoint (DataSubscriberInternal / RpcServerInternal) appeared or disappeared. On creation it is +// turned into a Link event once its parent (subscriber/server) and peer (publisher/client) are both known; if either +// is still missing it is remembered in the pending list. No Link removal event is emitted: a match teardown always +// coincides with a ServiceRemoved of one of its endpoints, from which the link's disappearance is inferred. +void ServiceObserver::HandleInternalMatch(Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor, + const std::string& parentIdKey, const std::string& peerUuid) +{ + using EventType = Discovery::ServiceDiscoveryEvent::Type; + + std::string parentIdStr; + if (!descriptor.GetSupplementalDataItem(parentIdKey, parentIdStr)) + { + return; + } + SilKit::Core::EndpointId parentServiceId{0}; + if (!TryParseEndpointId(parentIdStr, parentServiceId)) + { + return; + } + const ServiceKey parentKey{descriptor.GetParticipantName(), parentServiceId}; + + std::vector emissions; + { + std::lock_guard lock{_mutex}; + if (type == EventType::ServiceCreated) + { + const auto parentIt = _parents.find(parentKey); + const auto peerIt = _peersByUuid.find(peerUuid); + if (parentIt != _parents.end() && peerIt != _peersByUuid.end()) + { + emissions.push_back(MakeLink(parentIt->second, peerIt->second)); + } + else + { + _pending.push_back(PendingMatch{parentKey, peerUuid}); + } + } + else if (type == EventType::ServiceRemoved) + { + _pending.erase(std::remove_if(_pending.begin(), _pending.end(), + [&](const PendingMatch& p) { + return p.parentKey == parentKey && p.peerUuid == peerUuid; + }), + _pending.end()); + } + } + for (const auto& e : emissions) + { + EmitLink(e); + } +} + +// A peer (DataPublisher / RpcClient) appeared or disappeared. Its identity (keyed by its networkName / UUID) enables +// resolving matches. The peer's own ServiceCreated/ServiceRemoved is always emitted. +void ServiceObserver::HandlePeer(Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor) +{ + using EventType = Discovery::ServiceDiscoveryEvent::Type; + + const std::string peerUuid = descriptor.GetNetworkName(); + std::vector emissions; + { + std::lock_guard lock{_mutex}; + if (type == EventType::ServiceCreated) + { + _peersByUuid[peerUuid] = descriptor; + DrainResolvablePending(emissions); + } + else if (type == EventType::ServiceRemoved) + { + _peersByUuid.erase(peerUuid); + _pending.erase(std::remove_if(_pending.begin(), _pending.end(), + [&](const PendingMatch& p) { return p.peerUuid == peerUuid; }), + _pending.end()); + } + } + EmitService(ToC(type), descriptor); + for (const auto& e : emissions) + { + EmitLink(e); + } +} + +// A parent (DataSubscriber / RpcServer) appeared or disappeared. Its identity (keyed by participant + serviceId) +// enables resolving matches. The parent's own ServiceCreated/ServiceRemoved is always emitted. +void ServiceObserver::HandleParent(Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor) +{ + using EventType = Discovery::ServiceDiscoveryEvent::Type; + + const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; + std::vector emissions; + { + std::lock_guard lock{_mutex}; + if (type == EventType::ServiceCreated) + { + _parents[key] = descriptor; + DrainResolvablePending(emissions); + } + else if (type == EventType::ServiceRemoved) + { + _parents.erase(key); + _pending.erase(std::remove_if(_pending.begin(), _pending.end(), + [&](const PendingMatch& p) { return p.parentKey == key; }), + _pending.end()); + } + } + EmitService(ToC(type), descriptor); + for (const auto& e : emissions) + { + EmitLink(e); + } +} + +void ServiceObserver::HandleEvent(Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor) +{ + // Network-simulator links are reported directly as a Link service (created and removed: a detaching simulator + // leaves its controllers alive, so its removal is not otherwise observable). + if (descriptor.GetServiceType() == SilKit::Core::ServiceType::Link) + { + EmitService(ToC(type), descriptor); + return; + } + + std::string controllerType; + descriptor.GetSupplementalDataItem(Discovery::controllerType, controllerType); + + // Internal-match endpoints: a confirmed pub/sub or RPC match, surfaced as a Link event. + if (controllerType == Discovery::controllerTypeDataSubscriberInternal) + { + HandleInternalMatch(type, descriptor, Discovery::supplKeyDataSubscriberInternalParentServiceID, + descriptor.GetNetworkName()); + return; + } + if (controllerType == Discovery::controllerTypeRpcServerInternal) + { + std::string clientUuid; + descriptor.GetSupplementalDataItem(Discovery::supplKeyRpcServerInternalClientUUID, clientUuid); + HandleInternalMatch(type, descriptor, Discovery::supplKeyRpcServerInternalParentServiceID, clientUuid); + return; + } + + // Peers: DataPublisher / RpcClient (the connecting side of a match). + if (controllerType == Discovery::controllerTypeDataPublisher + || controllerType == Discovery::controllerTypeRpcClient) + { + HandlePeer(type, descriptor); + return; + } + + // Parents: DataSubscriber / RpcServer (the receiving side of a match). + if (controllerType == Discovery::controllerTypeDataSubscriber + || controllerType == Discovery::controllerTypeRpcServer) + { + HandleParent(type, descriptor); + return; + } + + // Bus controllers are emitted as-is; infrastructure / internal services are suppressed by ClassifyAndFill. + EmitService(ToC(type), descriptor); +} + +} // namespace VSilKit diff --git a/SilKit/source/capi/ServiceObserver.hpp b/SilKit/source/capi/ServiceObserver.hpp new file mode 100644 index 000000000..3db76e1d6 --- /dev/null +++ b/SilKit/source/capi/ServiceObserver.hpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "silkit/capi/Experimental.h" + +#include "core/internal/ServiceDescriptor.hpp" +#include "core/service/ServiceDatatypes.hpp" + +#include +#include +#include +#include +#include + +namespace VSilKit { + +// Translates the internal service-discovery events of a participant into the events of the +// experimental public service-discovery C API. It is driven from a single internal service-discovery +// handler (see CapiExperimental.cpp) and owns the small amount of state needed to correlate the raw +// internal announcements into fully-named public events. +// +// Reported events: +// - Bus controllers, publishers/subscribers and RPC clients/servers are forwarded as their own +// service kind on ServiceCreated / ServiceRemoved. +// - A confirmed pub/sub or RPC match (announced internally as a DataSubscriberInternal / +// RpcServerInternal endpoint) is surfaced as a SilKit_Experimental_ServiceKind_Link ServiceCreated +// event once both endpoints are known; no removal event is emitted for it (its teardown is implied +// by the ServiceRemoved of one of its endpoints). +// - A network-simulator link is surfaced as a SilKit_Experimental_ServiceKind_Link on both +// ServiceCreated and ServiceRemoved. +// - Infrastructure / internal services are suppressed. +class ServiceObserver +{ +public: + ServiceObserver(SilKit_Experimental_ServiceDiscoveryHandler_t handler, void* context); + + // Handle a single internal discovery event, emitting zero or more public events through the + // handler. Invocations are expected to be serialized (never concurrent) but may originate from + // different threads; the internal state is guarded by a mutex and the handler is always invoked + // outside that lock. + void HandleEvent(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor); + +private: + // Identifies a service on a participant: (participant name, service id). + using ServiceKey = std::pair; + + // An internal-match endpoint whose parent (subscriber/server) or peer (publisher/client) is not + // yet known. Resolved into a Link event once both descriptors have been discovered. + struct PendingMatch + { + ServiceKey parentKey; + std::string peerUuid; + }; + + // A resolved link ready to be emitted (holds the backing storage for the emitted struct). The + // parent descriptor is the receiving side (DataSubscriber / RpcServer); the connected... fields + // name the peer (DataPublisher / RpcClient). + struct LinkEmission + { + SilKit::Core::ServiceDescriptor parentDescriptor; + std::string connectedParticipantName; + std::string connectedServiceName; + }; + + void HandleInternalMatch(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor, const std::string& parentIdKey, + const std::string& peerUuid); + void HandlePeer(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor); + void HandleParent(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor); + + // Emits any pending match whose parent and peer are now both known. Called with _mutex held. + void DrainResolvablePending(std::vector& emissions); + + void EmitService(SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const SilKit::Core::ServiceDescriptor& descriptor); + void EmitLink(const LinkEmission& emission); + + static LinkEmission MakeLink(const SilKit::Core::ServiceDescriptor& parent, + const SilKit::Core::ServiceDescriptor& peer); + + SilKit_Experimental_ServiceDiscoveryHandler_t _handler{}; + void* _context{nullptr}; + + std::mutex _mutex; + // DataPublisher / RpcClient UUID (= their networkName) -> their descriptor. The UUID is globally + // unique, so publishers and clients share this map without collision. + std::map _peersByUuid; + // (participant, serviceId) -> DataSubscriber / RpcServer descriptor (the receiving side). + std::map _parents; + // Internal-match endpoints awaiting resolution of their parent and/or peer. + std::vector _pending; +}; + +} // namespace VSilKit diff --git a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp index c0e576928..dfebd12f7 100644 --- a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp +++ b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp @@ -2,20 +2,21 @@ // // SPDX-License-Identifier: MIT +// Tests for the thin CAPI wrapper around the service-discovery observer. The event-translation logic +// itself is covered by Test_ServiceObserver.cpp, which drives the VSilKit::ServiceObserver directly. +// Here we only verify parameter validation and that the CAPI wires the participant's internal service +// discovery through to the user handler. + #include -#include -#include #include "gtest/gtest.h" #include "gmock/gmock.h" #include "silkit/capi/SilKit.h" -#include "silkit/services/datatypes.hpp" #include "core/mock/participant/MockParticipant.hpp" #include "core/internal/ServiceDescriptor.hpp" #include "core/internal/ServiceConfigKeys.hpp" -#include "config/YamlParser.hpp" namespace { @@ -25,16 +26,6 @@ using SilKit::Core::ServiceType; using ServiceDiscoveryEvent = SilKit::Core::Discovery::ServiceDiscoveryEvent; namespace Discovery = SilKit::Core::Discovery; -struct CapturedLabel -{ - std::string key; - std::string value; - SilKit_LabelKind kind; -}; - -// Captures everything the public struct carries inside the handler invocation. Copying into owned -// storage also verifies that the struct and all its borrowed pointers are valid for the duration of -// the callback. struct CallbackData { int callCount{0}; @@ -42,16 +33,6 @@ struct CallbackData SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid}; SilKit_Experimental_ServiceKind serviceKind{SilKit_Experimental_ServiceKind_Undefined}; std::string participantName; - std::string serviceName; - std::string primaryIdentifier; - std::string mediaType; - std::vector labels; - std::string simulationName; - uint32_t numberOfConnections{0}; - std::string connectedParticipantName; - std::string connectedServiceName; - bool isSimulated{false}; - std::string simulatingParticipantName; }; void SilKitCALL CapturingHandler(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type type, @@ -62,21 +43,6 @@ void SilKitCALL CapturingHandler(void* context, SilKit_Experimental_ServiceDisco data->lastType = type; data->serviceKind = serviceDescriptor->serviceKind; data->participantName = serviceDescriptor->participantName; - data->serviceName = serviceDescriptor->serviceName; - data->primaryIdentifier = serviceDescriptor->primaryIdentifier; - data->mediaType = serviceDescriptor->mediaType; - data->labels.clear(); - for (size_t i = 0; i < serviceDescriptor->labelList.numLabels; ++i) - { - const auto& label = serviceDescriptor->labelList.labels[i]; - data->labels.push_back({label.key, label.value, label.kind}); - } - data->simulationName = serviceDescriptor->simulationName; - data->numberOfConnections = serviceDescriptor->numberOfConnections; - data->connectedParticipantName = serviceDescriptor->connectedParticipantName; - data->connectedServiceName = serviceDescriptor->connectedServiceName; - data->isSimulated = serviceDescriptor->isSimulated != SilKit_False; - data->simulatingParticipantName = serviceDescriptor->simulatingParticipantName; } void SilKitCALL NoopHandler(void* /*context*/, SilKit_Experimental_ServiceDiscoveryEvent_Type /*type*/, @@ -84,44 +50,10 @@ void SilKitCALL NoopHandler(void* /*context*/, SilKit_Experimental_ServiceDiscov { } -auto SerializeLabels(const std::vector& labels) -> std::string -{ - return SilKit::Config::Serialize(labels); -} - class Test_CapiServiceDiscovery : public testing::Test { public: DummyParticipant mockParticipant; - - static ServiceDescriptor MakeController(const std::string& controllerType, const std::string& networkName, - const std::string& serviceName) - { - ServiceDescriptor descriptor; - descriptor.SetParticipantNameAndComputeId("ParticipantA"); - descriptor.SetServiceName(serviceName); - descriptor.SetNetworkName(networkName); - descriptor.SetServiceType(ServiceType::Controller); - descriptor.SetSupplementalDataItem(Discovery::controllerType, controllerType); - return descriptor; - } - - // Registers the capturing C handler and returns the internal handler the C API installed on the service discovery. - SilKit::Core::Discovery::ServiceDiscoveryHandler InstallHandler(CallbackData& data) - { - SilKit::Core::Discovery::ServiceDiscoveryHandler capturedHandler; - EXPECT_CALL(mockParticipant.mockServiceDiscovery, RegisterServiceDiscoveryHandler(testing::_)) - .WillOnce(testing::SaveArg<0>(&capturedHandler)); - - SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; - EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), - SilKit_ReturnCode_SUCCESS); - EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, &data, - &CapturingHandler), - SilKit_ReturnCode_SUCCESS); - EXPECT_TRUE(static_cast(capturedHandler)); - return capturedHandler; - } }; TEST_F(Test_CapiServiceDiscovery, create_returns_service_discovery) @@ -151,650 +83,37 @@ TEST_F(Test_CapiServiceDiscovery, set_handler_bad_parameters) SilKit_ReturnCode_BADPARAMETER); } -TEST_F(Test_CapiServiceDiscovery, reports_can_controller) -{ - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeController(Discovery::controllerTypeCan, "CAN1", "Can1")); - - EXPECT_EQ(data.callCount, 1); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_CanController); - EXPECT_EQ(data.participantName, "ParticipantA"); - EXPECT_EQ(data.serviceName, "Can1"); - EXPECT_EQ(data.primaryIdentifier, "CAN1"); // bus controller: identifier is the network name - EXPECT_EQ(data.mediaType, ""); - EXPECT_TRUE(data.labels.empty()); - EXPECT_EQ(data.numberOfConnections, 0u); - EXPECT_FALSE(data.isSimulated); - EXPECT_EQ(data.simulatingParticipantName, ""); -} - -TEST_F(Test_CapiServiceDiscovery, reports_data_publisher_with_decoded_metadata) -{ - CallbackData data; - auto handler = InstallHandler(data); - - auto descriptor = MakeController(Discovery::controllerTypeDataPublisher, "pub-uuid-1234", "MyPublisher"); - descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "TopicA"); - descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherMediaType, "application/json"); - descriptor.SetSupplementalDataItem( - Discovery::supplKeyDataPublisherPubLabels, - SerializeLabels({{"kA", "vA", SilKit::Services::MatchingLabel::Kind::Mandatory}, - {"kB", "vB", SilKit::Services::MatchingLabel::Kind::Optional}})); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); - - EXPECT_EQ(data.callCount, 1); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); - EXPECT_EQ(data.primaryIdentifier, "TopicA"); // pub/sub: identifier is the topic - EXPECT_EQ(data.mediaType, "application/json"); - ASSERT_EQ(data.labels.size(), 2u); - EXPECT_EQ(data.labels[0].key, "kA"); - EXPECT_EQ(data.labels[0].value, "vA"); - EXPECT_EQ(data.labels[0].kind, SilKit_LabelKind_Mandatory); - EXPECT_EQ(data.labels[1].key, "kB"); - EXPECT_EQ(data.labels[1].kind, SilKit_LabelKind_Optional); -} - -TEST_F(Test_CapiServiceDiscovery, reports_rpc_client_with_function_name) +// The CAPI must register a handler on the participant's internal service discovery and route the +// events it delivers through to the user's C handler (via a ServiceObserver). +TEST_F(Test_CapiServiceDiscovery, wires_internal_events_to_user_handler) { - CallbackData data; - auto handler = InstallHandler(data); - - auto descriptor = MakeController(Discovery::controllerTypeRpcClient, "rpc-uuid-9", "MyClient"); - descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientFunctionName, "Add"); - descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientMediaType, "application/octet-stream"); + SilKit::Core::Discovery::ServiceDiscoveryHandler internalHandler; + EXPECT_CALL(mockParticipant.mockServiceDiscovery, RegisterServiceDiscoveryHandler(testing::_)) + .WillOnce(testing::SaveArg<0>(&internalHandler)); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); - - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_RpcClient); - EXPECT_EQ(data.primaryIdentifier, "Add"); // rpc: identifier is the function name - EXPECT_EQ(data.mediaType, "application/octet-stream"); -} + SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), + SilKit_ReturnCode_SUCCESS); -TEST_F(Test_CapiServiceDiscovery, network_link_events_are_suppressed) -{ - // Network links are not reported directly at the public API level. Instead they drive - // isSimulated / Service_Updated events on bus controllers (see is_simulated_* tests below). CallbackData data; - auto handler = InstallHandler(data); + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, &data, + &CapturingHandler), + SilKit_ReturnCode_SUCCESS); + ASSERT_TRUE(static_cast(internalHandler)); ServiceDescriptor descriptor; descriptor.SetParticipantNameAndComputeId("ParticipantA"); - descriptor.SetServiceName("LinkService"); + descriptor.SetServiceName("Can1"); descriptor.SetNetworkName("CAN1"); - descriptor.SetServiceType(ServiceType::Link); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); - - EXPECT_EQ(data.callCount, 0); -} - -TEST_F(Test_CapiServiceDiscovery, reports_removal) -{ - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeController(Discovery::controllerTypeLin, "LIN1", "Lin1")); - - EXPECT_EQ(data.callCount, 1); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_LinController); -} - -TEST_F(Test_CapiServiceDiscovery, filters_infrastructure_and_internal_services) -{ - CallbackData data; - auto handler = InstallHandler(data); - - // Infrastructure controller (SystemMonitor) is not user-facing. - handler(ServiceDiscoveryEvent::Type::ServiceCreated, - MakeController(Discovery::controllerTypeSystemMonitor, "default", "SystemMonitor")); - // DataSubscriberInternal with no parent id is silently dropped. - handler(ServiceDiscoveryEvent::Type::ServiceCreated, - MakeController(Discovery::controllerTypeDataSubscriberInternal, "sub-uuid", "SubInternal")); - - // A controller without a controller.type supplemental entry is skipped, too. - ServiceDescriptor noControllerType; - noControllerType.SetParticipantNameAndComputeId("ParticipantA"); - noControllerType.SetServiceName("Mystery"); - noControllerType.SetServiceType(ServiceType::Controller); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, noControllerType); - - EXPECT_EQ(data.callCount, 0); -} - -TEST_F(Test_CapiServiceDiscovery, malformed_labels_are_swallowed) -{ - CallbackData data; - auto handler = InstallHandler(data); - - auto descriptor = MakeController(Discovery::controllerTypeDataPublisher, "pub-uuid", "MyPublisher"); - descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "TopicA"); - descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherPubLabels, "{ this is : not [ valid"); - - // Must not throw across the FFI boundary; the service is still reported. - EXPECT_NO_THROW(handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor)); - EXPECT_EQ(data.callCount, 1); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); - EXPECT_EQ(data.primaryIdentifier, "TopicA"); -} - -// -------------------------------------------------------------------------------------------------- -// DataSubscriber: reported immediately on creation; Service_Updated fires on each connection change. -// -------------------------------------------------------------------------------------------------- - -namespace { - -auto MakeUserSubscriber(SilKit::Core::EndpointId serviceId, const std::string& topic) -> ServiceDescriptor -{ - ServiceDescriptor descriptor; - descriptor.SetParticipantNameAndComputeId("SubParticipant"); - descriptor.SetServiceName("Sub"); - descriptor.SetNetworkName("default"); - descriptor.SetServiceType(ServiceType::Controller); - descriptor.SetServiceId(serviceId); - descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataSubscriber); - descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberTopic, topic); - return descriptor; -} - -// DataSubscriberInternal: a confirmed match between a publisher (identified by its UUID = networkName) -// and the parent DataSubscriber. -auto MakeInternalConnection(SilKit::Core::EndpointId serviceId, SilKit::Core::EndpointId parentServiceId, - const std::string& publisherUuid = "pub-uuid") -> ServiceDescriptor -{ - ServiceDescriptor descriptor; - descriptor.SetParticipantNameAndComputeId("SubParticipant"); - descriptor.SetServiceName("SubInternal"); - descriptor.SetNetworkName(publisherUuid); - descriptor.SetServiceType(ServiceType::Controller); - descriptor.SetServiceId(serviceId); - descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataSubscriberInternal); - descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberInternalParentServiceID, - std::to_string(parentServiceId)); - return descriptor; -} - -auto MakeDataPublisher(const std::string& publisherUuid, const std::string& participantName, - const std::string& serviceName) -> ServiceDescriptor -{ - ServiceDescriptor descriptor; - descriptor.SetParticipantNameAndComputeId(participantName); - descriptor.SetServiceName(serviceName); - descriptor.SetNetworkName(publisherUuid); - descriptor.SetServiceType(ServiceType::Controller); - descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataPublisher); - descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "T"); - return descriptor; -} - -auto MakeRpcServer(SilKit::Core::EndpointId serviceId, const std::string& functionName) -> ServiceDescriptor -{ - ServiceDescriptor descriptor; - descriptor.SetParticipantNameAndComputeId("ServerParticipant"); - descriptor.SetServiceName("RpcSrv"); - descriptor.SetNetworkName("srv-net"); - descriptor.SetServiceType(ServiceType::Controller); - descriptor.SetServiceId(serviceId); - descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcServer); - descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerFunctionName, functionName); - return descriptor; -} - -auto MakeRpcClient(const std::string& clientUuid, const std::string& participantName, - const std::string& serviceName) -> ServiceDescriptor -{ - ServiceDescriptor descriptor; - descriptor.SetParticipantNameAndComputeId(participantName); - descriptor.SetServiceName(serviceName); - descriptor.SetNetworkName(clientUuid); descriptor.SetServiceType(ServiceType::Controller); - descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcClient); - descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientFunctionName, "Add"); - return descriptor; -} + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeCan); -auto MakeRpcServerInternal(SilKit::Core::EndpointId serviceId, SilKit::Core::EndpointId parentServiceId, - const std::string& clientUuid) -> ServiceDescriptor -{ - ServiceDescriptor descriptor; - descriptor.SetParticipantNameAndComputeId("ServerParticipant"); - descriptor.SetServiceName("RpcSrvInternal"); - descriptor.SetNetworkName("srv-internal-net"); - descriptor.SetServiceType(ServiceType::Controller); - descriptor.SetServiceId(serviceId); - descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcServerInternal); - descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerInternalParentServiceID, - std::to_string(parentServiceId)); - descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerInternalClientUUID, clientUuid); - return descriptor; -} - -auto MakeBusController(const std::string& ctrlType, const std::string& networkName, const std::string& serviceName, - SilKit::Core::EndpointId serviceId, - const std::string& participantName = "CtrlParticipant") -> ServiceDescriptor -{ - ServiceDescriptor descriptor; - descriptor.SetParticipantNameAndComputeId(participantName); - descriptor.SetServiceName(serviceName); - descriptor.SetNetworkName(networkName); - descriptor.SetServiceType(ServiceType::Controller); - descriptor.SetServiceId(serviceId); - descriptor.SetSupplementalDataItem(Discovery::controllerType, ctrlType); - return descriptor; -} - -auto MakeNetworkLink(const std::string& networkName, const std::string& simulatorParticipant) -> ServiceDescriptor -{ - ServiceDescriptor descriptor; - descriptor.SetParticipantNameAndComputeId(simulatorParticipant); - descriptor.SetServiceName(networkName + "_link"); - descriptor.SetNetworkName(networkName); - descriptor.SetServiceType(ServiceType::Link); - return descriptor; -} - -} // namespace - -TEST_F(Test_CapiServiceDiscovery, subscriber_reported_immediately_on_creation) -{ - // A DataSubscriber is visible as soon as it is created, even without any connections. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + internalHandler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); EXPECT_EQ(data.callCount, 1); EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); - EXPECT_EQ(data.primaryIdentifier, "T"); - EXPECT_EQ(data.numberOfConnections, 0u); - EXPECT_EQ(data.connectedParticipantName, ""); - EXPECT_EQ(data.connectedServiceName, ""); -} - -TEST_F(Test_CapiServiceDiscovery, subscriber_service_updated_on_connection) -{ - // When a DataSubscriberInternal confirms a match, a Service_Updated with the new count fires. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); - ASSERT_EQ(data.callCount, 1); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); - - EXPECT_EQ(data.callCount, 2); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); - EXPECT_EQ(data.primaryIdentifier, "T"); - EXPECT_EQ(data.numberOfConnections, 1u); -} - -TEST_F(Test_CapiServiceDiscovery, subscriber_service_updated_shows_peer_info) -{ - // The Service_Updated event for a DataSubscriber connection names the publisher as the peer. - CallbackData data; - auto handler = InstallHandler(data); - - const std::string uuid = "pub-uuid-abc"; - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeDataPublisher(uuid, "PubParticipant", "MyPub")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); - - ASSERT_EQ(data.callCount, 3); // publisher + subscriber + service_updated - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_EQ(data.connectedParticipantName, "PubParticipant"); - EXPECT_EQ(data.connectedServiceName, "MyPub"); - EXPECT_EQ(data.numberOfConnections, 1u); -} - -TEST_F(Test_CapiServiceDiscovery, subscriber_service_updated_on_disconnection) -{ - // When the DataSubscriberInternal is removed the count decrements via another Service_Updated. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); - ASSERT_EQ(data.callCount, 2); - - handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(200, 100)); - - EXPECT_EQ(data.callCount, 3); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_EQ(data.numberOfConnections, 0u); -} - -TEST_F(Test_CapiServiceDiscovery, subscriber_service_removed_on_removal) -{ - // ServiceRemoved fires when the DataSubscriber itself is removed, not on last-connection loss. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); - ASSERT_EQ(data.callCount, 2); - - handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeUserSubscriber(100, "T")); - - EXPECT_EQ(data.callCount, 3); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); -} - -TEST_F(Test_CapiServiceDiscovery, subscriber_connection_arrives_before_subscriber) -{ - // When the DataSubscriberInternal arrives before the DataSubscriber, the subscriber is reported - // as ServiceCreated (with connections already counted) once the subscriber itself arrives. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); - EXPECT_EQ(data.callCount, 0); // subscriber not known yet - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); - EXPECT_EQ(data.callCount, 1); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); - EXPECT_EQ(data.numberOfConnections, 1u); // connection already counted -} - -TEST_F(Test_CapiServiceDiscovery, subscriber_multiple_connections_counted) -{ - // Each new DataSubscriberInternal fires a Service_Updated with an incremented count. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100)); - EXPECT_EQ(data.numberOfConnections, 1u); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(201, 100)); - EXPECT_EQ(data.callCount, 3); - EXPECT_EQ(data.numberOfConnections, 2u); - - handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(200, 100)); - EXPECT_EQ(data.numberOfConnections, 1u); - - handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(201, 100)); - EXPECT_EQ(data.numberOfConnections, 0u); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_EQ(data.callCount, 5); -} - -TEST_F(Test_CapiServiceDiscovery, subscriber_peer_revealed_when_publisher_arrives_late) -{ - // A connection observed before its publisher: the count is reported immediately without a peer, - // and the peer identity is revealed by a later Service_Updated once the publisher is discovered. - // This is what lets an observer that attaches to a running simulation reconstruct the graph. - CallbackData data; - auto handler = InstallHandler(data); - - const std::string uuid = "pub-uuid-late"; - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); - - // Count is known, peer is not yet. - EXPECT_EQ(data.numberOfConnections, 1u); - EXPECT_EQ(data.connectedParticipantName, ""); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeDataPublisher(uuid, "PubParticipant", "MyPub")); - - // The last event is the reveal: a Service_Updated on the subscriber naming the publisher. - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); - EXPECT_EQ(data.numberOfConnections, 1u); - EXPECT_EQ(data.connectedParticipantName, "PubParticipant"); - EXPECT_EQ(data.connectedServiceName, "MyPub"); -} - -TEST_F(Test_CapiServiceDiscovery, subscriber_created_reveals_peer_for_preexisting_connection) -{ - // Adverse replay order (attach-to-running): the internal connection and the publisher are both - // seen before the subscriber. When the subscriber finally arrives, it is reported created and the - // pre-existing connection's peer is revealed on a following Service_Updated. - CallbackData data; - auto handler = InstallHandler(data); - - const std::string uuid = "pub-uuid-replay"; - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeDataPublisher(uuid, "PubParticipant", "MyPub")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); - - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); - EXPECT_EQ(data.numberOfConnections, 1u); - EXPECT_EQ(data.connectedParticipantName, "PubParticipant"); - EXPECT_EQ(data.connectedServiceName, "MyPub"); -} - -TEST_F(Test_CapiServiceDiscovery, reports_data_subscriber_with_decoded_labels) -{ - // The subscriber-side label key must be decoded too (not just the publisher's). - CallbackData data; - auto handler = InstallHandler(data); - - auto descriptor = MakeController(Discovery::controllerTypeDataSubscriber, "default", "MySubscriber"); - descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberTopic, "TopicS"); - descriptor.SetSupplementalDataItem( - Discovery::supplKeyDataSubscriberSubLabels, - SerializeLabels({{"sk", "sv", SilKit::Services::MatchingLabel::Kind::Mandatory}})); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); - - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); - EXPECT_EQ(data.primaryIdentifier, "TopicS"); - ASSERT_EQ(data.labels.size(), 1u); - EXPECT_EQ(data.labels[0].key, "sk"); - EXPECT_EQ(data.labels[0].value, "sv"); - EXPECT_EQ(data.labels[0].kind, SilKit_LabelKind_Mandatory); -} - -TEST_F(Test_CapiServiceDiscovery, reports_ethernet_and_flexray_controllers) -{ - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, - MakeController(Discovery::controllerTypeEthernet, "ETH1", "Eth1")); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_EthernetController); - EXPECT_EQ(data.primaryIdentifier, "ETH1"); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, - MakeController(Discovery::controllerTypeFlexray, "FR1", "Fr1")); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_FlexrayController); - EXPECT_EQ(data.primaryIdentifier, "FR1"); -} - -// -------------------------------------------------------------------------------------------------- -// RpcServer: reported immediately on creation; Service_Updated fires on each RpcServerInternal event. -// -------------------------------------------------------------------------------------------------- - -TEST_F(Test_CapiServiceDiscovery, rpc_server_reported_immediately_on_creation) -{ - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); - - EXPECT_EQ(data.callCount, 1); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_RpcServer); - EXPECT_EQ(data.primaryIdentifier, "Add"); - EXPECT_EQ(data.numberOfConnections, 0u); -} - -TEST_F(Test_CapiServiceDiscovery, rpc_server_service_updated_on_connection) -{ - // When a RpcServerInternal confirms a call match, a Service_Updated fires on the parent server. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); - ASSERT_EQ(data.callCount, 1); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, "client-uuid-1")); - - EXPECT_EQ(data.callCount, 2); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_RpcServer); - EXPECT_EQ(data.numberOfConnections, 1u); -} - -TEST_F(Test_CapiServiceDiscovery, rpc_server_service_updated_shows_peer_info) -{ - // The Service_Updated for an RPC connection names the client as the peer. - CallbackData data; - auto handler = InstallHandler(data); - - const std::string clientUuid = "client-uuid-xyz"; - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcClient(clientUuid, "ClientParticipant", "MyClient")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, clientUuid)); - - ASSERT_EQ(data.callCount, 3); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_EQ(data.connectedParticipantName, "ClientParticipant"); - EXPECT_EQ(data.connectedServiceName, "MyClient"); - EXPECT_EQ(data.numberOfConnections, 1u); -} - -TEST_F(Test_CapiServiceDiscovery, rpc_server_peer_revealed_when_client_arrives_late) -{ - // RPC analogue of the pub/sub late-peer reveal: the server connection is seen before the client; - // the client's identity is revealed by a later Service_Updated once the client is discovered. - CallbackData data; - auto handler = InstallHandler(data); - - const std::string clientUuid = "client-uuid-late"; - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, clientUuid)); - - EXPECT_EQ(data.numberOfConnections, 1u); - EXPECT_EQ(data.connectedParticipantName, ""); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcClient(clientUuid, "ClientParticipant", "MyClient")); - - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_RpcServer); - EXPECT_EQ(data.numberOfConnections, 1u); - EXPECT_EQ(data.connectedParticipantName, "ClientParticipant"); - EXPECT_EQ(data.connectedServiceName, "MyClient"); -} - -TEST_F(Test_CapiServiceDiscovery, rpc_server_service_updated_on_disconnection) -{ - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, "client-uuid")); - ASSERT_EQ(data.callCount, 2); - - handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeRpcServerInternal(60, 50, "client-uuid")); - - EXPECT_EQ(data.callCount, 3); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_EQ(data.numberOfConnections, 0u); -} - -// -------------------------------------------------------------------------------------------------- -// Network simulator detection: bus controllers carry isSimulated / simulatingParticipantName. -// No NetworkLink event type is exposed at the public API level. -// -------------------------------------------------------------------------------------------------- - -TEST_F(Test_CapiServiceDiscovery, is_simulated_false_for_controller_without_link) -{ - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, - MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); - - EXPECT_EQ(data.callCount, 1); - EXPECT_FALSE(data.isSimulated); - EXPECT_EQ(data.simulatingParticipantName, ""); -} - -TEST_F(Test_CapiServiceDiscovery, is_simulated_set_when_link_arrives_after_controller) -{ - // When a network simulator announces itself after the bus controller, a Service_Updated fires. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, - MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); - ASSERT_EQ(data.callCount, 1); - ASSERT_FALSE(data.isSimulated); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); - - EXPECT_EQ(data.callCount, 2); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_CanController); - EXPECT_TRUE(data.isSimulated); - EXPECT_EQ(data.simulatingParticipantName, "SimParticipant"); -} - -TEST_F(Test_CapiServiceDiscovery, is_simulated_set_on_service_created_when_link_precedes_controller) -{ - // When the network link is already active before the bus controller arrives, the ServiceCreated - // event for that controller already carries isSimulated = true — no separate Service_Updated needed. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); - EXPECT_EQ(data.callCount, 0); // link itself not reported - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, - MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); - - EXPECT_EQ(data.callCount, 1); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); - EXPECT_TRUE(data.isSimulated); - EXPECT_EQ(data.simulatingParticipantName, "SimParticipant"); -} - -TEST_F(Test_CapiServiceDiscovery, is_simulated_cleared_on_link_removal) -{ - // When the network simulator disappears, a Service_Updated clears isSimulated. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, - MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); - ASSERT_EQ(data.callCount, 2); - ASSERT_TRUE(data.isSimulated); - - handler(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeNetworkLink("CAN1", "SimParticipant")); - - EXPECT_EQ(data.callCount, 3); - EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceUpdated); - EXPECT_FALSE(data.isSimulated); - EXPECT_EQ(data.simulatingParticipantName, ""); -} - -TEST_F(Test_CapiServiceDiscovery, link_fan_out_reaches_all_controllers_on_network) -{ - // A single link event fans out to every bus controller on that network. - CallbackData data; - auto handler = InstallHandler(data); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, - MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); - handler(ServiceDiscoveryEvent::Type::ServiceCreated, - MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can2", 2)); - ASSERT_EQ(data.callCount, 2); - - handler(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); - - // Both controllers receive a Service_Updated. - EXPECT_EQ(data.callCount, 4); + EXPECT_EQ(data.participantName, "ParticipantA"); } } // namespace diff --git a/SilKit/source/capi/Test_ServiceObserver.cpp b/SilKit/source/capi/Test_ServiceObserver.cpp new file mode 100644 index 000000000..f44edf58b --- /dev/null +++ b/SilKit/source/capi/Test_ServiceObserver.cpp @@ -0,0 +1,561 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include +#include + +#include "gtest/gtest.h" + +#include "silkit/capi/SilKit.h" +#include "silkit/services/datatypes.hpp" + +#include "capi/ServiceObserver.hpp" + +#include "core/internal/ServiceDescriptor.hpp" +#include "core/internal/ServiceConfigKeys.hpp" +#include "config/YamlParser.hpp" + +namespace { + +using SilKit::Core::ServiceDescriptor; +using SilKit::Core::ServiceType; +using ServiceDiscoveryEvent = SilKit::Core::Discovery::ServiceDiscoveryEvent; +namespace Discovery = SilKit::Core::Discovery; + +struct CapturedLabel +{ + std::string key; + std::string value; + SilKit_LabelKind kind; +}; + +// A single captured event. Copying into owned storage also verifies that the struct and all its +// borrowed pointers are valid for the duration of the callback. +struct CapturedEvent +{ + SilKit_Experimental_ServiceDiscoveryEvent_Type type{SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid}; + SilKit_Experimental_ServiceKind serviceKind{SilKit_Experimental_ServiceKind_Undefined}; + std::string participantName; + std::string serviceName; + std::string primaryIdentifier; + std::string mediaType; + std::vector labels; + std::string simulationName; + std::string connectedParticipantName; + std::string connectedServiceName; +}; + +// Captures every event the public handler receives, plus the latest one for convenience. +struct CallbackData +{ + int callCount{0}; + std::vector events; + + const CapturedEvent& last() const + { + return events.back(); + } +}; + +void SilKitCALL CapturingHandler(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor) +{ + auto* data = static_cast(context); + data->callCount += 1; + + CapturedEvent event; + event.type = type; + event.serviceKind = serviceDescriptor->serviceKind; + event.participantName = serviceDescriptor->participantName; + event.serviceName = serviceDescriptor->serviceName; + event.primaryIdentifier = serviceDescriptor->primaryIdentifier; + event.mediaType = serviceDescriptor->mediaType; + for (size_t i = 0; i < serviceDescriptor->labelList.numLabels; ++i) + { + const auto& label = serviceDescriptor->labelList.labels[i]; + event.labels.push_back({label.key, label.value, label.kind}); + } + event.simulationName = serviceDescriptor->simulationName; + event.connectedParticipantName = serviceDescriptor->connectedParticipantName; + event.connectedServiceName = serviceDescriptor->connectedServiceName; + + data->events.push_back(std::move(event)); +} + +auto SerializeLabels(const std::vector& labels) -> std::string +{ + return SilKit::Config::Serialize(labels); +} + +auto MakeController(const std::string& controllerType, const std::string& networkName, + const std::string& serviceName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ParticipantA"); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(networkName); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetSupplementalDataItem(Discovery::controllerType, controllerType); + return descriptor; +} + +auto MakeUserSubscriber(SilKit::Core::EndpointId serviceId, const std::string& topic) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("SubParticipant"); + descriptor.SetServiceName("Sub"); + descriptor.SetNetworkName("default"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataSubscriber); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberTopic, topic); + return descriptor; +} + +// DataSubscriberInternal: a confirmed match between a publisher (identified by its UUID = networkName) +// and the parent DataSubscriber. +auto MakeInternalConnection(SilKit::Core::EndpointId serviceId, SilKit::Core::EndpointId parentServiceId, + const std::string& publisherUuid = "pub-uuid") -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("SubParticipant"); + descriptor.SetServiceName("SubInternal"); + descriptor.SetNetworkName(publisherUuid); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataSubscriberInternal); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberInternalParentServiceID, + std::to_string(parentServiceId)); + return descriptor; +} + +auto MakeDataPublisher(const std::string& publisherUuid, const std::string& participantName, + const std::string& serviceName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(participantName); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(publisherUuid); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataPublisher); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "T"); + return descriptor; +} + +auto MakeRpcServer(SilKit::Core::EndpointId serviceId, const std::string& functionName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ServerParticipant"); + descriptor.SetServiceName("RpcSrv"); + descriptor.SetNetworkName("srv-net"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcServer); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerFunctionName, functionName); + return descriptor; +} + +auto MakeRpcClient(const std::string& clientUuid, const std::string& participantName, + const std::string& serviceName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(participantName); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(clientUuid); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcClient); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientFunctionName, "Add"); + return descriptor; +} + +auto MakeRpcServerInternal(SilKit::Core::EndpointId serviceId, SilKit::Core::EndpointId parentServiceId, + const std::string& clientUuid) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ServerParticipant"); + descriptor.SetServiceName("RpcSrvInternal"); + descriptor.SetNetworkName("srv-internal-net"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcServerInternal); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerInternalParentServiceID, + std::to_string(parentServiceId)); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerInternalClientUUID, clientUuid); + return descriptor; +} + +auto MakeBusController(const std::string& ctrlType, const std::string& networkName, const std::string& serviceName, + SilKit::Core::EndpointId serviceId, + const std::string& participantName = "CtrlParticipant") -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(participantName); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(networkName); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, ctrlType); + return descriptor; +} + +auto MakeNetworkLink(const std::string& networkName, const std::string& simulatorParticipant) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(simulatorParticipant); + descriptor.SetServiceName(networkName + "_link"); + descriptor.SetNetworkName(networkName); + descriptor.SetServiceType(ServiceType::Link); + return descriptor; +} + +// Drives the ServiceObserver directly: no CAPI wrapper and no mock participant. The observer emits +// through the C handler into `data`. +class Test_ServiceObserver : public testing::Test +{ +public: + CallbackData data; + VSilKit::ServiceObserver observer{&CapturingHandler, &data}; +}; + +// -------------------------------------------------------------------------------------------------- +// Basic classification. +// -------------------------------------------------------------------------------------------------- + +TEST_F(Test_ServiceObserver, reports_can_controller) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeCan, "CAN1", "Can1")); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_CanController); + EXPECT_EQ(data.last().participantName, "ParticipantA"); + EXPECT_EQ(data.last().serviceName, "Can1"); + EXPECT_EQ(data.last().primaryIdentifier, "CAN1"); // bus controller: identifier is the network name + EXPECT_EQ(data.last().mediaType, ""); + EXPECT_TRUE(data.last().labels.empty()); + EXPECT_EQ(data.last().connectedParticipantName, ""); + EXPECT_EQ(data.last().connectedServiceName, ""); +} + +TEST_F(Test_ServiceObserver, reports_data_publisher_with_decoded_metadata) +{ + auto descriptor = MakeController(Discovery::controllerTypeDataPublisher, "pub-uuid-1234", "MyPublisher"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "TopicA"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherMediaType, "application/json"); + descriptor.SetSupplementalDataItem( + Discovery::supplKeyDataPublisherPubLabels, + SerializeLabels({{"kA", "vA", SilKit::Services::MatchingLabel::Kind::Mandatory}, + {"kB", "vB", SilKit::Services::MatchingLabel::Kind::Optional}})); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); + EXPECT_EQ(data.last().primaryIdentifier, "TopicA"); // pub/sub: identifier is the topic + EXPECT_EQ(data.last().mediaType, "application/json"); + ASSERT_EQ(data.last().labels.size(), 2u); + EXPECT_EQ(data.last().labels[0].key, "kA"); + EXPECT_EQ(data.last().labels[0].value, "vA"); + EXPECT_EQ(data.last().labels[0].kind, SilKit_LabelKind_Mandatory); + EXPECT_EQ(data.last().labels[1].key, "kB"); + EXPECT_EQ(data.last().labels[1].kind, SilKit_LabelKind_Optional); +} + +TEST_F(Test_ServiceObserver, reports_rpc_client_with_function_name) +{ + auto descriptor = MakeController(Discovery::controllerTypeRpcClient, "rpc-uuid-9", "MyClient"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientFunctionName, "Add"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientMediaType, "application/octet-stream"); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_RpcClient); + EXPECT_EQ(data.last().primaryIdentifier, "Add"); // rpc: identifier is the function name + EXPECT_EQ(data.last().mediaType, "application/octet-stream"); +} + +TEST_F(Test_ServiceObserver, reports_removal) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceRemoved, + MakeController(Discovery::controllerTypeLin, "LIN1", "Lin1")); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_LinController); +} + +TEST_F(Test_ServiceObserver, filters_infrastructure_and_internal_services) +{ + // Infrastructure controller (SystemMonitor) is not user-facing. + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeSystemMonitor, "default", "SystemMonitor")); + // DataSubscriberInternal with no parent id is silently dropped (no match to report). + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeDataSubscriberInternal, "sub-uuid", "SubInternal")); + + // A controller without a controller.type supplemental entry is skipped, too. + ServiceDescriptor noControllerType; + noControllerType.SetParticipantNameAndComputeId("ParticipantA"); + noControllerType.SetServiceName("Mystery"); + noControllerType.SetServiceType(ServiceType::Controller); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, noControllerType); + + EXPECT_EQ(data.callCount, 0); +} + +TEST_F(Test_ServiceObserver, malformed_labels_are_swallowed) +{ + auto descriptor = MakeController(Discovery::controllerTypeDataPublisher, "pub-uuid", "MyPublisher"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "TopicA"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherPubLabels, "{ this is : not [ valid"); + + // Must not throw across the FFI boundary; the service is still reported. + EXPECT_NO_THROW(observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor)); + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); + EXPECT_EQ(data.last().primaryIdentifier, "TopicA"); +} + +TEST_F(Test_ServiceObserver, reports_data_subscriber_with_decoded_labels) +{ + auto descriptor = MakeController(Discovery::controllerTypeDataSubscriber, "default", "MySubscriber"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberTopic, "TopicS"); + descriptor.SetSupplementalDataItem( + Discovery::supplKeyDataSubscriberSubLabels, + SerializeLabels({{"sk", "sv", SilKit::Services::MatchingLabel::Kind::Mandatory}})); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + EXPECT_EQ(data.last().primaryIdentifier, "TopicS"); + ASSERT_EQ(data.last().labels.size(), 1u); + EXPECT_EQ(data.last().labels[0].key, "sk"); + EXPECT_EQ(data.last().labels[0].value, "sv"); + EXPECT_EQ(data.last().labels[0].kind, SilKit_LabelKind_Mandatory); +} + +TEST_F(Test_ServiceObserver, reports_ethernet_and_flexray_controllers) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeEthernet, "ETH1", "Eth1")); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_EthernetController); + EXPECT_EQ(data.last().primaryIdentifier, "ETH1"); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeFlexray, "FR1", "Fr1")); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_FlexrayController); + EXPECT_EQ(data.last().primaryIdentifier, "FR1"); +} + +// -------------------------------------------------------------------------------------------------- +// Pub/sub matches: reported as a Link ServiceCreated event once both endpoints are known. +// -------------------------------------------------------------------------------------------------- + +TEST_F(Test_ServiceObserver, subscriber_reported_immediately_on_creation) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + EXPECT_EQ(data.last().primaryIdentifier, "T"); + EXPECT_EQ(data.last().connectedParticipantName, ""); + EXPECT_EQ(data.last().connectedServiceName, ""); +} + +TEST_F(Test_ServiceObserver, pubsub_match_reported_as_link) +{ + const std::string uuid = "pub-uuid-abc"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + + ASSERT_EQ(data.callCount, 3); // publisher + subscriber + link + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().participantName, "SubParticipant"); // receiving side + EXPECT_EQ(data.last().serviceName, "Sub"); + EXPECT_EQ(data.last().primaryIdentifier, "T"); + EXPECT_EQ(data.last().connectedParticipantName, "PubParticipant"); // peer + EXPECT_EQ(data.last().connectedServiceName, "MyPub"); +} + +TEST_F(Test_ServiceObserver, pubsub_match_deferred_until_publisher_arrives) +{ + const std::string uuid = "pub-uuid-late"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + EXPECT_EQ(data.callCount, 1); // only the subscriber so far; the match is pending + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + + ASSERT_EQ(data.callCount, 3); // publisher event + the now-resolvable link + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().connectedParticipantName, "PubParticipant"); + EXPECT_EQ(data.last().connectedServiceName, "MyPub"); +} + +TEST_F(Test_ServiceObserver, pubsub_match_deferred_until_subscriber_arrives) +{ + const std::string uuid = "pub-uuid-replay"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + EXPECT_EQ(data.callCount, 0); // nothing user-facing yet + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + EXPECT_EQ(data.callCount, 1); // publisher, but subscriber still unknown + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + + ASSERT_EQ(data.callCount, 3); // subscriber event + the now-resolvable link + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().connectedParticipantName, "PubParticipant"); + EXPECT_EQ(data.last().connectedServiceName, "MyPub"); +} + +TEST_F(Test_ServiceObserver, pubsub_match_no_link_removed_event) +{ + const std::string uuid = "pub-uuid-teardown"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + ASSERT_EQ(data.callCount, 3); + + // Removing the internal match endpoint produces no event. + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(200, 100, uuid)); + EXPECT_EQ(data.callCount, 3); + + // Removing the publisher is observable, and lets the consumer infer the link is gone. + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceRemoved, + MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + ASSERT_EQ(data.callCount, 4); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); +} + +TEST_F(Test_ServiceObserver, subscriber_service_removed_on_removal) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + ASSERT_EQ(data.callCount, 1); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeUserSubscriber(100, "T")); + + ASSERT_EQ(data.callCount, 2); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); +} + +TEST_F(Test_ServiceObserver, pubsub_multiple_matches_each_reported_as_link) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeDataPublisher("uuid-1", "PubA", "PubCtrlA")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeDataPublisher("uuid-2", "PubB", "PubCtrlB")); + const int before = data.callCount; + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, "uuid-1")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(201, 100, "uuid-2")); + + ASSERT_EQ(data.callCount, before + 2); + EXPECT_EQ(data.events[before].serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.events[before].connectedParticipantName, "PubA"); + EXPECT_EQ(data.events[before + 1].serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.events[before + 1].connectedParticipantName, "PubB"); +} + +// -------------------------------------------------------------------------------------------------- +// RPC matches: reported as a Link ServiceCreated event once both endpoints are known. +// -------------------------------------------------------------------------------------------------- + +TEST_F(Test_ServiceObserver, rpc_server_reported_immediately_on_creation) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_RpcServer); + EXPECT_EQ(data.last().primaryIdentifier, "Add"); +} + +TEST_F(Test_ServiceObserver, rpc_match_reported_as_link) +{ + const std::string clientUuid = "client-uuid-xyz"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeRpcClient(clientUuid, "ClientParticipant", "MyClient")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, clientUuid)); + + ASSERT_EQ(data.callCount, 3); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().participantName, "ServerParticipant"); // receiving side + EXPECT_EQ(data.last().serviceName, "RpcSrv"); + EXPECT_EQ(data.last().primaryIdentifier, "Add"); + EXPECT_EQ(data.last().connectedParticipantName, "ClientParticipant"); // peer + EXPECT_EQ(data.last().connectedServiceName, "MyClient"); +} + +TEST_F(Test_ServiceObserver, rpc_match_deferred_until_client_arrives) +{ + const std::string clientUuid = "client-uuid-late"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, clientUuid)); + EXPECT_EQ(data.callCount, 1); // only the server; the match is pending + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeRpcClient(clientUuid, "ClientParticipant", "MyClient")); + + ASSERT_EQ(data.callCount, 3); // client event + the now-resolvable link + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().connectedParticipantName, "ClientParticipant"); + EXPECT_EQ(data.last().connectedServiceName, "MyClient"); +} + +// -------------------------------------------------------------------------------------------------- +// Network-simulator links: reported as a Link service (created and removed). +// -------------------------------------------------------------------------------------------------- + +TEST_F(Test_ServiceObserver, network_link_reported_as_link_service) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().participantName, "SimParticipant"); + EXPECT_EQ(data.last().primaryIdentifier, "CAN1"); // network name, joins to bus controllers' identifier +} + +TEST_F(Test_ServiceObserver, network_link_removal_reported) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); + ASSERT_EQ(data.callCount, 1); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeNetworkLink("CAN1", "SimParticipant")); + + ASSERT_EQ(data.callCount, 2); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().primaryIdentifier, "CAN1"); +} + +TEST_F(Test_ServiceObserver, bus_controller_reported_independently_of_link) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); + + ASSERT_EQ(data.callCount, 2); + EXPECT_EQ(data.events[0].serviceKind, SilKit_Experimental_ServiceKind_CanController); + EXPECT_EQ(data.events[0].primaryIdentifier, "CAN1"); + EXPECT_EQ(data.events[1].serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.events[1].primaryIdentifier, "CAN1"); // same network -> the controller is simulated +} + +} // namespace