diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 631bbdda..7e045359 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: check-executables-have-shebangs - id: check-added-large-files args: [--maxkb=50, --enforce-all] # increase or add git lfs if too strict - exclude: MODULE.bazel.lock|tests/benchmarks/benchmarks_analysis.md + exclude: MODULE.bazel.lock|tests/benchmarks/benchmarks_analysis.md|src/socom/test/unit/runtime_tests.cpp - repo: https://github.com/jumanjihouse/pre-commit-hooks rev: 38980559e3a605691d6579f96222c30778e5a69e # 3.0.0 diff --git a/src/socom/BUILD b/src/socom/BUILD new file mode 100644 index 00000000..fa4baee7 --- /dev/null +++ b/src/socom/BUILD @@ -0,0 +1,54 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +filegroup( + name = "headers", + srcs = glob(include = ["include/**"]), +) + +cc_library( + name = "socom", + srcs = glob(include = ["src/**"]), + hdrs = [":headers"], + defines = [ + "WITH_SOCOM_DEADLOCK_DETECTION=1", + ], + features = COMPILER_WARNING_FEATURES, + includes = [ + "include", + "src/socom/include", + ], + visibility = ["//visibility:public"], + deps = [ + "@score_baselibs//score/result", + ], +) + +filegroup( + name = "mock_headers", + srcs = glob(include = ["mock/**"]), +) + +cc_library( + name = "mock", + hdrs = [":mock_headers"], + includes = ["mock"], + visibility = ["//visibility:public"], + deps = [ + ":socom", + "@googletest//:gtest_for_library", + ], +) diff --git a/src/socom/README.adoc b/src/socom/README.adoc new file mode 100644 index 00000000..e6d8811c --- /dev/null +++ b/src/socom/README.adoc @@ -0,0 +1,36 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + += Service Oriented Communication (SOCom) +:description: SOCom +:docinfo: shared +:diagramdir: models +:imagesdir: images +:toc: left +:iconsdir: images +:icons: font +:sectnums: +:sectnumlevels: 5 + +The bundle service oriented communication (SOCom) implements components for client/server pattern based communication for the following communication paradigms: + +* remote procedure call; +* publish/subscribe (event communication). + +SOCom provides interfaces which enable the implementation of service gateways (bridges). +This allows users to independently implement service bridges for different communication protocols (e.g. IPC, SOME/IP, …). +SOCom provides intra-process client/server communication only. + +== Design + +See <> for a high level design. diff --git a/src/socom/doc/design/30_structural_view.adoc b/src/socom/doc/design/30_structural_view.adoc new file mode 100644 index 00000000..530b3bc6 --- /dev/null +++ b/src/socom/doc/design/30_structural_view.adoc @@ -0,0 +1,81 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//// +Every asciidoc file in this document starts with the section level 0, i.e. a section like +.... += Title +.... + +When the file is included from another file within the document, the levels must be adjusted with the `leveloffset` attribute to the `include` directive at the point of inclusion, e.g. +.... +include::file.adoc[leveloffset=+1] +.... +//// + += Structural View + +The bundle service oriented communication (SOCom) implements components for client/server pattern based communication for the following communication paradigms: + +* remote procedure call; +* publish/subscribe (event communication). + +SOCom provides interfaces which enable the implementation of service gateways (bridges). +This allows users to independently implement service bridges for different communication protocols (e.g. IPC, SOME/IP, …). +SOCom provides intra-process client/server communication only. + +.Component diagram of SOCom +[plantuml, svg, align="center"] +.... +include::models/component_diagram_socom.puml[] +.... + +.Software elements + +* <>; +* <>; +* <>; + +[#runtime_component] +== Runtime + +The Runtime creates and connects Client_connectors and Server_connectors. +In addition to that it has a plugin interface for adding bridges to cross IPC or network boundaries. +The Runtime must outlive all created Client_connectors and Server_connectors. + +[#client_connector_component] +== Client connector + +A client application owns and uses this component to join client/server pattern based service oriented communication (SOCom). +The client connector interacts with the server connector in order perform the supported communication primitives. +The client connector API provides the following features: + +* service instance state change indications +* asynchronous remote procedure call + +** with method reply +** without method reply (fire and forget) + +* event subscription + +** with optional initial value request (required for fields) + +* event subscription acknowledge indication +* event update indication +* requested event update indication + +[#server_connector_component] +== Server connector + +The Server_connector represents a service, which is used by a Client_connector. +It can send event updates and respond to method calls. diff --git a/src/socom/doc/design/40_behavioral_view.adoc b/src/socom/doc/design/40_behavioral_view.adoc new file mode 100644 index 00000000..19a4e9cf --- /dev/null +++ b/src/socom/doc/design/40_behavioral_view.adoc @@ -0,0 +1,172 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//// +Every asciidoc file in this document starts with the section level 0, i.e. a section like +.... += Title +.... + +When the file is included from another file within the document, the levels must be adjusted with the `leveloffset` attribute to the `include` directive at the point of inclusion, e.g. +.... +include::file.adoc[leveloffset=+1] +.... +//// + += Behavioral View + +This component implements the following state machines: + +* <>; +* <>. + +== Runtime + +The runtime is a (process internal) service instance broker. +Client connectors and server connectors are created using the runtime (factory), which keeps track of their existence and state. +Depending on the existence and state of the communication partners, the runtime takes care for connecting/disconnecting service interface and service instance compatible client and server connectors. +Additionally, it provides a service instance find interface. + +== Server connector + +A server application owns and uses this component to join client/server pattern based service oriented communication (SOCom). +The server connector interacts with the client connector in order perform the supported communication primitives. +The server connector API provides the following features: + +* remote procedure called indication; +* event subscription state changed indication: + +** on first subscriber; +** on last unsubscriber. + +* event update request indication; +* event mode getter (event with/without initial update); +* update event; +* update requested event; +* acknowledge event subscription. + +== Method Communication + +Methods calls are routed 1:1 from client to server application. +If a method reply is requested, the calling client is served with the related reply. +Independent method calls share no state. + +.Interaction diagram: Method communication +[plantuml, svg, align="center"] +.... +include::models/interaction_diagram_method_communication.puml[] +.... + +== Event Communication + +The client subscribes to an event. +On the subscription, SOCom informs the server using the request for events. +If no client is subscribed to an event, the server will not send any event updates. + +After unsubscription of the subscriber, SOCom informs the server that an event is not subscribed anymore. +SOCom keeps track of subscription states and related clients. + +.Interaction diagram: Event communication +[plantuml, svg, align="center"] +.... +include::models/interaction_diagram_event_communication.puml[] +.... + +== Field Notification Communication + +A field notification is implemented as an event with event update on subscription. + +NOTE: Additional behavior compared to field communication: +since SOCom does not save the current event values, SOCom requests an event update from the server for each new subscriber. + +Servers answer this request with update_requested_event(). +Those updates are indicated to the client which is subscribed and has an initial event update pending. + +Regular update_event() calls update for the events of the subscriber and also fulfills the initial event update use-case. +Consequently, update_event() satisfies the event update request implicitly. + +.Interaction diagram: Field notification communication +[plantuml, svg, align="center"] +.... +include::models/interaction_diagram_field_notification_communication.puml[] +.... + +== Service Gateway - find service + +The creation of client connectors is forwarded to bridges as request_service() calls. +Service bridges look up the required service instance within their domain. +If available, they connect to the remote counterpart and locally create a proxy server connector (forwarding all communication). + +.Interaction diagram: Service Gateway - find service +[plantuml, svg, align="center"] +.... +include::models/interaction_diagram_service_gateway_find_service.puml[] +.... + +== Service Gateway - require service + +The creation of client connectors is forwarded to bridges as request_service() calls. +Service bridges look up the required service instance within their domain. +If available, they connect to the remote counterpart and locally create a proxy server connector (forwarding all communication). + +.Interaction diagram: Service Gateway - require service +[plantuml, svg, align="center"] +.... +include::models/interaction_diagram_service_gateway_require_service.puml[] +.... + +== Service Gateway - provide service + +The creation of server connectors is found by service bridges using the SOCom subscribe_find_service() API. +Service bridges provide this information within their domain. +If any domain partner requests the service instance, the service bridge creates a proxy client connector (forwarding all communication). + +.Interaction diagram: Service Gateway - provide service +[plantuml, svg, align="center"] +.... +include::models/interaction_diagram_service_gateway_provide_service.puml[] +.... + +== Deadlock detection + +In order to facilitate deadlock detection, before a callback is called by a Client_connector or +Server_connector the thread id of the caller is saved. +After the callback returns, the previously saved thread id is removed. +If the calling object is destructed prematurely the deadlock is detected by checking if the thread +id is still present, issuing a warning log and terminating the application. + +== Client_connector deadlocks + +When a running callback destroys the calling Client_connector a deadlock will happen, which will cause the application to be terminated. + +The deadlock is caused because the Client_connector destructor waits for the callback to return. +The callback on the other hand waits for the destructor to return. + +.Interaction diagram: Client_connector deadlocks +[plantuml, svg, align="center"] +.... +include::models/interaction_diagram_client_connector_deadlocks.puml[] +.... + +== Server_connector deadlocks + +When a running callback destroys the calling Server_connector a deadlock will happen, which will cause the application to be terminated. + +The deadlock is caused because the Server_connector destructor waits for the callback to return. +The callback on the other hand waits for the destructor to return. + +.Interaction diagram: Server_connector deadlocks +[plantuml, svg, align="center"] +.... +include::models/interaction_diagram_server_connector_deadlocks.puml[] +.... diff --git a/src/socom/doc/design/50_state_dynamics_view.adoc b/src/socom/doc/design/50_state_dynamics_view.adoc new file mode 100644 index 00000000..e628edb6 --- /dev/null +++ b/src/socom/doc/design/50_state_dynamics_view.adoc @@ -0,0 +1,115 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//// +Every asciidoc file in this document starts with the section level 0, i.e. a section like +.... += Title +.... + +When the file is included from another file within the document, the levels must be adjusted with the `leveloffset` attribute to the `include` directive at the point of inclusion, e.g. +.... +include::file.adoc[leveloffset=+1] +.... +//// + += State Dynamics View + +[#event_subscription_state_machine] +== Event subscription state + +The following model describes the state-behavior of an event subscription from a logical perspective. + +.State diagram of event subscription state +[plantuml, svg, align="center"] +.... +include::models/state_diagram_event_subscription_state.puml[] +.... + +.States +|==== +| Name | Description + +| Not_available +| The client and server are not connected. +No event subscription or transmission is possible. + +| Available +| The client and server are connected and the event is not subscribed. +No event update will be received in this state. + +| Event_subscribed +| The user is subscribed to the event. +Event updates will be received in this state. +However, the server has not acknowledged the subscription yet. +Thus clients do not know if the server will send event updates. +|==== + +.Triggers +|==== +| Name | Description + +| Client_connector::subscribe_event() +| The user calls Client_connector::subscribe_event() in order to subscribe to an event. + +| Client_connector::unsubscribe_event() +| The user calls Client_connector::unsubscribe_event() in order to unsubscribe from an event. + +| Client_connector::Callbacks::on_service_state_change(Service_state::available) +| SOCom calls the user callback Client_connector::Callbacks::on_service_state_change(Service_state::available). + +| Client_connector::Callbacks::on_service_state_change(!Service_state::available) +| SOCom calls the user callback Client_connector::Callbacks::on_service_state_change() with a state different from Service_state::available. +|==== + +[#service_state_machine] +== Service state + +The following model describes the state-behavior of a service instance as reported to an instance of type Client_connector by callback on_state_change(). + +.State diagram of service state +[plantuml, svg, align="center"] +.... +include::models/state_diagram_service_state.puml[] +.... + +.States +|==== +| Name | Description + +| Service_state::not_available +| The service instance does not exist. +No provided service instance within the service domain is known. + +| Service_state::available +| The service instance exists, is connected and available. +A provided service instance is known, identified and enabled. +Thus the service instance can be used and service requests will be answered. +|==== + +.Triggers +|==== +| Name | Description + +| Runtime::make_server_connector() +| Construction of Disabled_server_connector by API function Runtime::make_server_connector(). + +| Disabled_server_connector::enable() +| Disabling of Enabled_server_connector by API function Enabled_server_connector::disable(). + +| ~Disabled_server_connector() +| Destruction of Disabled_server_connector(). + +| ~Enabled_server_connector() +| Destruction of Enabled_server_connector(). +|==== diff --git a/src/socom/doc/design/README.adoc b/src/socom/doc/design/README.adoc new file mode 100644 index 00000000..f078292d --- /dev/null +++ b/src/socom/doc/design/README.adoc @@ -0,0 +1,39 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + += Software design SOCom library +:description: SOCom library software element +:docinfo: shared +:diagramdir: models +:imagesdir: images +:toc: left +:iconsdir: images +:icons: font +:sectnums: +:sectnumlevels: 5 + +// keep the structure of this document +// 1st line = Document main title +// 2nd line = Document sub title (this is what you see in web directory) +// don't touch all the other attributes, they are needed by asciidoc +// store external PUML files always to a models sub directory +// store images always to a images sub directory +// in this file shall only be include what is part of HTML documentation only + + +[#software_element_socom] +== SOCom library + +include::30_structural_view.adoc[leveloffset=+1] +include::40_behavioral_view.adoc[leveloffset=+1] +include::50_state_dynamics_view.adoc[leveloffset=+1] diff --git a/src/socom/doc/design/models/component_diagram_socom.puml b/src/socom/doc/design/models/component_diagram_socom.puml new file mode 100644 index 00000000..76ad016f --- /dev/null +++ b/src/socom/doc/design/models/component_diagram_socom.puml @@ -0,0 +1,36 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +left to right direction +package SOCom { + Component Client_connector + Component Server_connector + Component Runtime + Component Service_interface_definition +} + +() "Client_connector" as Client_connector_if +() "Disabled_server_connector" as Disabled_server_connector_if +() "Enabled_server_connector" as Enabled_server_connector_if +() "Runtime" as Runtime_if + +Client_connector_if -- Client_connector +Disabled_server_connector_if -- Server_connector +Enabled_server_connector_if -- Server_connector +Runtime_if -- Runtime + +Client_connector --> Service_interface_definition : depends +Server_connector --> Service_interface_definition : depends +Runtime --> Service_interface_definition : depends +@enduml diff --git a/src/socom/doc/design/models/interaction_diagram_client_connector_deadlocks.puml b/src/socom/doc/design/models/interaction_diagram_client_connector_deadlocks.puml new file mode 100644 index 00000000..37ff70f2 --- /dev/null +++ b/src/socom/doc/design/models/interaction_diagram_client_connector_deadlocks.puml @@ -0,0 +1,122 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +participant Client_callbacks +participant Client_connector +participant Server_connector + +== Deadlock by on_service_state_change == + +Server_connector --> Client_connector: state_changed() +activate Client_connector + +Client_connector --> Client_callbacks: on_service_state_change() +activate Client_callbacks + +Client_callbacks --> Client_connector: ~Client_connector() +destroy Client_connector +deactivate Client_callbacks + +rnote over Client_connector + Deadlock happens here because destructor waits for callback to finish. +endrnote + +== Deadlock by on_event_update == + +Server_connector --> Client_connector: update_event() +activate Client_connector + +Client_connector --> Client_callbacks: on_event_update() +activate Client_callbacks + +Client_callbacks --> Client_connector: ~Client_connector() +destroy Client_connector +deactivate Client_callbacks + +rnote over Client_connector + Deadlock happens here because destructor waits for callback to finish. +endrnote + +== Deadlock by on_event_requested_update == + +Server_connector --> Client_connector: update_requested_event() +activate Client_connector + +Client_connector --> Client_callbacks: on_event_requested_update() +activate Client_callbacks + +Client_callbacks --> Client_connector: ~Client_connector() +destroy Client_connector +deactivate Client_callbacks + +rnote over Client_connector + Deadlock happens here because destructor waits for callback to finish. +endrnote + +== Deadlock by on_event_subscription_status_change == + +Server_connector --> Client_connector: set_event_subscription_state() +activate Client_connector + +Client_connector --> Client_callbacks: on_event_subscription_status_change () +activate Client_callbacks + +Client_callbacks --> Client_connector: ~Client_connector() +destroy Client_connector +deactivate Client_callbacks + +rnote over Client_connector + Deadlock happens here because destructor waits for callback to finish. +endrnote + +== Deadlock by on_method_reply at client == + +Client_connector --> Server_connector: call_method() +activate Client_connector +activate Server_connector + +Server_connector --> Client_connector: method_reply() + +Client_connector --> Client_callbacks: on_method_reply() +activate Client_callbacks + +Client_callbacks --> Client_connector: ~Client_connector() +destroy Client_connector +deactivate Client_callbacks +deactivate Server_connector + +rnote over Client_connector + Deadlock happens here because destructor waits for callback to finish. +endrnote + +== Deadlock by on_method_reply at server == + +Client_connector --> Server_connector: call_method() +activate Client_connector +activate Server_connector + +Server_connector --> Client_connector: method_reply() + +Client_connector --> Client_callbacks: on_method_reply() +activate Client_callbacks + +Client_callbacks --> Server_connector: ~Server_connector() +deactivate Client_connector +deactivate Client_callbacks +destroy Server_connector + +rnote over Client_connector + Deadlock happens here because destructor waits for callback to finish. +endrnote +@enduml diff --git a/src/socom/doc/design/models/interaction_diagram_event_communication.puml b/src/socom/doc/design/models/interaction_diagram_event_communication.puml new file mode 100644 index 00000000..dda5e47c --- /dev/null +++ b/src/socom/doc/design/models/interaction_diagram_event_communication.puml @@ -0,0 +1,63 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +participant Client_1 <> +participant SOCom +participant Server <> +autonumber +activate Server + +activate Client_1 + +Client_1 -> SOCom : subscribe_event(x, update) +activate SOCom + +SOCom -> Server : on_event_subscription_change(x, subscribed) +activate Server + +return + +return + +== ... == + +Server -> Server : updated event + +Server -> SOCom : allocate_event_payload(x) +activate SOCom +SOCom -> Client_1 : on_event_payload_allocate(x) +activate Client_1 +return writable_payload +return writable_payload + +Server -> SOCom : update_event(x, payload) +activate SOCom + +SOCom -> Client_1 : on_event_update(x, payload) +activate Client_1 +return + +return + +== ... == + +Client_1 -> SOCom : unsubscribe_event(x) +activate SOCom + +SOCom -> Server : on_event_subscription_change(x, unsubscribed) +activate Server +return + +return +@enduml diff --git a/src/socom/doc/design/models/interaction_diagram_field_notification_communication.puml b/src/socom/doc/design/models/interaction_diagram_field_notification_communication.puml new file mode 100644 index 00000000..efde36d3 --- /dev/null +++ b/src/socom/doc/design/models/interaction_diagram_field_notification_communication.puml @@ -0,0 +1,71 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +participant Client_1 <> +participant SOCom +participant Server <> + +autonumber + +activate Server +activate Client_1 + +Server -> Server : field_update(x, payload) + +Client_1 -> SOCom : subscribe_event(x, update_and_initial_value) + +activate SOCom + +SOCom -> Server : on_event_subscription_change(x, subscribed) +activate Server +return + +SOCom -> Server : request_event_update(x) +activate Server +return + +return + +Server -> SOCom : update_requested_event(x, payload) +activate SOCom +SOCom -> Client_1 : on_requested_event_update(x, payload) +activate Client_1 +return +return + +== ... == + +Server -> Server : field_update(x, payload) + +Server -> SOCom : update_event(x, payload) +activate SOCom + +SOCom -> Client_1 : on_event_update(x, payload) +activate Client_1 +return + +return + +== ... == + +Client_1 -> SOCom : unsubscribe_event(x) +activate SOCom + +SOCom -> Server : on_event_subscription_change(x, unsubscribed) +activate Server +return + +return +deactivate Client_1 +@enduml diff --git a/src/socom/doc/design/models/interaction_diagram_method_communication.puml b/src/socom/doc/design/models/interaction_diagram_method_communication.puml new file mode 100644 index 00000000..ef8b7d4a --- /dev/null +++ b/src/socom/doc/design/models/interaction_diagram_method_communication.puml @@ -0,0 +1,70 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +participant Client_1 <> +participant SOCom +participant Server <> +autonumber +activate Server +activate Client_1 + +== Reply requested == + +Client_1 -> SOCom : allocate_method_call_payload(x) +activate SOCom +SOCom -> Server : on_method_call_payload_allocate(x) +activate Server +return writable_payload +return writable_payload + +Client_1 -> SOCom : call_method(x, call_payload, reply_data) + +activate SOCom +SOCom -> Server : on_method_call(x, call_payload, reply_data) +activate Server +Server -> Server : process method x +return +return + +note right of Server + Server might write data into reply_data.reply_payload +end note + +Server -> SOCom : reply_data.reply_callback(reply_data.reply_payload) +activate SOCom +SOCom -> Client_1 : on_method_reply(x, reply_payload) +activate Client_1 +return +SOCom --> Server + +deactivate SOCom +== Fire and forget (no reply) == + +Client_1 -> SOCom : allocate_method_call_payload(x) +activate SOCom +SOCom -> Server : on_method_call_payload_allocate(x) +activate Server +return writable_payload +return writable_payload + +Client_1 -> SOCom : call_method(x, call_payload, nullopt) +activate SOCom + +SOCom -> Server : on_method_call(x, call_payload, nullopt) +activate Server + +Server -> Server : process method x +return +return +@enduml diff --git a/src/socom/doc/design/models/interaction_diagram_server_connector_deadlocks.puml b/src/socom/doc/design/models/interaction_diagram_server_connector_deadlocks.puml new file mode 100644 index 00000000..1e7d928d --- /dev/null +++ b/src/socom/doc/design/models/interaction_diagram_server_connector_deadlocks.puml @@ -0,0 +1,70 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +participant Server_callbacks +participant Server_connector +participant Client_connector + +' Method_call_callback on_method_call; +' Event_subscription_change_callback on_event_subscription_change; +' Event_request_update_callback on_event_update_request; + +== Deadlock by on_method_call == + +Client_connector --> Server_connector: call_method() +activate Server_connector + +Server_connector --> Server_callbacks: on_method_call() +activate Server_callbacks + +Server_callbacks --> Server_connector: ~Server_connector() +destroy Server_connector +deactivate Server_callbacks + +rnote over Server_connector + Deadlock happens here because destructor waits for callback to finish. +endrnote + +== Deadlock by on_event_subscription_change == + +Client_connector --> Server_connector: subscribe_event() +activate Server_connector + +Server_connector --> Server_callbacks: on_event_subscription_change() +activate Server_callbacks + +Server_callbacks --> Server_connector: ~Server_connector() +destroy Server_connector +deactivate Server_callbacks + +rnote over Server_connector + Deadlock happens here because destructor waits for callback to finish. +endrnote + +== Deadlock by on_event_update_request == + +Client_connector --> Server_connector: request_event_update() +activate Server_connector + +Server_connector --> Server_callbacks: on_event_update_request() +activate Server_callbacks + +Server_callbacks --> Server_connector: ~Server_connector() +destroy Server_connector +deactivate Server_callbacks + +rnote over Server_connector + Deadlock happens here because destructor waits for callback to finish. +endrnote +@enduml diff --git a/src/socom/doc/design/models/interaction_diagram_service_gateway_find_service.puml b/src/socom/doc/design/models/interaction_diagram_service_gateway_find_service.puml new file mode 100644 index 00000000..076560d7 --- /dev/null +++ b/src/socom/doc/design/models/interaction_diagram_service_gateway_find_service.puml @@ -0,0 +1,88 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +participant app as "Application" <> +participant socom as "SOCom" +participant bridge as "Generic service bridge" <> +autonumber + +== Initialization == + +activate app +app -> socom ** : create_runtime() + +app -> bridge ** : create_bridge_component() +activate bridge + +bridge -> socom : register_service_bridge\n(bridge_subscribe_find_service, bridge_request_service) +activate socom +return + +bridge -> socom : subscribe_find_service\n(bridge_find_callback) +activate socom +return + +note right +A generic service bridge (e.g. bridge::ipc) +is interested in every service instance +within the local process. +This enables a config-free (dynamic) +implementation of service discovery functionality +based on the pure internal service instance identifiers. +end note + + +== Application searches for a service instance == + +app -> socom : subscribe_find_service\n(app_find_callback, interface_A, instance_1) +activate socom +socom -> bridge : bridge_subscribe_find_service\n(socom_find_callback, interface_A, instance_1) +activate bridge + +group Implementation detail +bridge -> bridge +note right +The generic service bridge +searches for the service instance on +its remote counterparts. +end note +end +return + +return + + +== service instance remote search result changed (added)== + +group Implementation detail +bridge -> bridge +note right +Found a service instance on a remote counterpart. +end note +end + +bridge -> socom : socom_find_callback\n(interface_A, instance_1, State::added) +activate socom +socom -> app : app_find_callback\n(interface_A, instance_1, State::added) +activate app +return + +return + +note right +Important: SOCom does not forward find results to +service-interface wildcard finds +(subscribe_find_service(callback)). +end note +@enduml diff --git a/src/socom/doc/design/models/interaction_diagram_service_gateway_provide_service.puml b/src/socom/doc/design/models/interaction_diagram_service_gateway_provide_service.puml new file mode 100644 index 00000000..3c3728a8 --- /dev/null +++ b/src/socom/doc/design/models/interaction_diagram_service_gateway_provide_service.puml @@ -0,0 +1,95 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +participant app as "Application" <> +participant socom as "SOCom" +participant bridge as "Generic service bridge" <> +autonumber + +== Initialization == + +activate app +app -> socom ** : create_runtime() +activate socom +app -> bridge ** : create_bridge_component() +activate bridge + +bridge -> socom : register_service_bridge(bridge_subscribe_find_service, bridge_request_service) +activate socom +return + +bridge -> socom : subscribe_find_service(bridge_find_callback) +activate socom +return + +note right +A generic service bridge (e.g. bridge::ipc) +is interested in every service instance +within the local process. +This enables a config-free (dynamic) +implementation of service discovery functionality +based on the pure internal service instance identifiers. +end note + + +== Application creates a service instance == + +app -> socom : make_server_connector(interface_A, instance_1) +activate socom +socom -> bridge : bridge_find_callback(interface_A, instance_1, State::added) +activate bridge + +group Implementation detail +bridge -> bridge +note right +The generic service bridge +informs its remote counterparts +about the availability +of a new service instance +interface_A, instance_1 +at the local endpoint. +end note +end +return + +return + +== service instance remote usage == + +group Implementation detail +bridge -> bridge +note right +A remote counterpart is interested +in the local service and informed the +generic service bridge about. +end note +end + +bridge -> socom : make_client_connector(interface_A, instance_1) +activate socom +return + +group !!! Important: break circle !!! +socom X--> bridge : bridge_request_service(interface_A, instance_1) +note right #FFAAAA +SOCom shall call bridge_request_service() +only if service instance is locally not available. +end note +end + +socom -> socom +note right +SOCom connects client_connector to server_connector. +end note +@enduml diff --git a/src/socom/doc/design/models/interaction_diagram_service_gateway_require_service.puml b/src/socom/doc/design/models/interaction_diagram_service_gateway_require_service.puml new file mode 100644 index 00000000..f5a1e7d2 --- /dev/null +++ b/src/socom/doc/design/models/interaction_diagram_service_gateway_require_service.puml @@ -0,0 +1,93 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +participant app as "Application" <> +participant socom as "SOCom" +participant bridge as "Generic service bridge" <> +autonumber + +== Initialization == + +activate app +app -> socom ** : create_runtime() +activate socom + +app -> bridge ** : create_bridge_component() +activate bridge + +bridge -> socom : register_service_bridge\n(bridge_subscribe_find_service, bridge_request_service) +activate socom +return + +bridge -> socom : subscribe_find_service(bridge_find_callback) +activate socom +return + +note right +A generic service bridge (e.g. bridge::ipc) +is interested in every service instance +within the local process. +This enables a config-free (dynamic) +implementation of service discovery functionality +based on the pure internal service instance identifiers. +end note + + +== Application requests a service instance == + +app -> socom : make_client_connector(interface_A, instance_1) +activate socom +socom -> bridge : bridge_request_service(interface_A, instance_1) +activate bridge + +group Implementation detail +bridge -> bridge +note right +The generic service bridge +requests for the service instance on +its remote counterparts. +end note +end +return + +app <-- socom +deactivate socom + +== remote service instance connect == + +group Implementation detail +bridge -> bridge +note right +The generic service bridge connects to a remote counterpart +which hosts the service instance. +end note +end + +bridge -> socom : make_server_connector(interface_A, instance_1) +activate socom +return + +group !!! Important: break circle !!! +socom -X bridge : bridge_find_callback(interface_A, instance_1, State::added) +note right #FFAAAA +The bridge shall detect that find result is +caused by it's own created server_connector. +end note +end + +socom -> socom +note right +SOCom connects client_connector to server_connector. +end note +@enduml diff --git a/src/socom/doc/design/models/state_diagram_event_subscription_state.puml b/src/socom/doc/design/models/state_diagram_event_subscription_state.puml new file mode 100644 index 00000000..dd158287 --- /dev/null +++ b/src/socom/doc/design/models/state_diagram_event_subscription_state.puml @@ -0,0 +1,30 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +hide empty description + +state Not_available +state Available +state Event_subscribed + +[*] --> Not_available +Not_available --> Available: Client_connector::Callbacks::\non_service_state_change\n(Service_state::available) + +Available --> Not_available: Client_connector::Callbacks::\non_service_state_change\n(!Service_state::available) +Available --> Event_subscribed: Client_connector::\nsubscribe_event() + +Event_subscribed --> Available: Client_connector::\nunsubscribe_event() +Event_subscribed --> Not_available: Client_connector::Callbacks::\non_service_state_change\n(!Service_state::available) + +@enduml diff --git a/src/socom/doc/design/models/state_diagram_service_state.puml b/src/socom/doc/design/models/state_diagram_service_state.puml new file mode 100644 index 00000000..8a56320c --- /dev/null +++ b/src/socom/doc/design/models/state_diagram_service_state.puml @@ -0,0 +1,29 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +hide empty description + +state "Service_state::not_available" as not_available +state "Service_state::available" as available + +[*] --> not_available + +not_available --> not_available: Runtime::make_server_connector() +not_available --> available: Disabled_server_connector::enable() + +available --> not_available: Enabled_server_connector::disable() +available --> not_available: \~Enabled_server_connector() + +not_available --> not_available: \~Disabled_server_connector(): +@enduml diff --git a/src/socom/doc/design/models/zero_copy_event_update.puml b/src/socom/doc/design/models/zero_copy_event_update.puml new file mode 100644 index 00000000..255d4e6e --- /dev/null +++ b/src/socom/doc/design/models/zero_copy_event_update.puml @@ -0,0 +1,109 @@ +' ******************************************************************************* +' Copyright (c) 2025 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml Zero Copy Event Update from Network + +actor network as "Network" + +box Network Daemon + +participant someip_network_impl as "SOME/IP Network implementation" +participant socom_server_nd as "socom::Server_connector" +participant socom_client_nd as "socom::Client_connector" + +end box + +participant ipc as "Message passing IPC" + +box Gateway Daemon + +participant socom_server_gd as "socom::Server_connector" +participant socom_client_gd as "socom::Client_connector" +participant gateway as "Gatway logic" +participant mwcom as "mw::com" + +end box + +note right network +socom was design with a 1:n relationship. An event update by a Server_connector +is distributed to multiple Client_connectors. However this does not work well +with zero-copy. + +For zero-copy there needs to be a 1:1 relationship between Server_connector +and Client_connector. The assumption is that Message passing / mw::com +will do the multiplexing if needed. +end note + +== Read event from network into IPC buffer == + +network -> someip_network_impl : Receive event update notification + +activate someip_network_impl +someip_network_impl -> socom_server_nd : allocate_event_update() +activate socom_server_nd +socom_server_nd -> socom_client_nd : allocate_payload() + +note right +Call is actually callback through Client_connector and does not really invoke Client_connector code. +Actually the Server_connector calls a callback which directly uses Message Passing to allocate the buffer. + +This applies to all Client_connector calls in this diagram. +end note + +activate socom_client_nd +socom_client_nd -> ipc : allocate_ipc_buffer() +activate ipc +return Buffer +return Buffer +return Buffer +someip_network_impl -> network : Read event update into buffer + +== Send event update via IPC to Gateway Daemon == + +someip_network_impl -> socom_server_nd : update_event(event_id, Buffer) +activate socom_server_nd +socom_server_nd -> socom_client_nd : update_event(event_id, Buffer) +activate socom_client_nd +socom_client_nd -> ipc : send_event_update(event_id, Buffer) +activate ipc +return +return +return +deactivate someip_network_impl + +== Gateway Daemon allocates buffer for event update == + +ipc -> socom_server_gd : receive_event_update(event_id, Buffer) +activate socom_server_gd +socom_server_gd -> socom_client_gd : allocate_event_payload(event_id) +activate socom_client_gd +socom_client_gd -> gateway : allocate() +activate gateway +gateway -> mwcom : allocate(event_id) +activate mwcom +return Buffer +return Buffer +return Buffer + +== Gateway Daemon processes event update == + +socom_server_gd -> socom_client_gd : update_event(event_id, Buffer) +activate socom_client_gd +socom_client_gd -> gateway : update_event(event_id, Buffer) +activate gateway +gateway -> gateway : E2E check +gateway -> gateway : Process event update + +gateway -> mwcom : send(event_id, processed Buffer) + +@enduml diff --git a/src/socom/include/score/socom/client_connector.hpp b/src/socom/include/score/socom/client_connector.hpp new file mode 100644 index 00000000..e8e4abf3 --- /dev/null +++ b/src/socom/include/score/socom/client_connector.hpp @@ -0,0 +1,249 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SRC_SOCOM_INCLUDE_SCORE_SOCOM_CLIENT_CONNECTOR +#define SRC_SOCOM_INCLUDE_SCORE_SOCOM_CLIENT_CONNECTOR + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace score::socom { + +class Client_connector; + +/// \brief Service states from the service user viewpoint. +enum class Service_state : std::uint8_t { + /// Service is not available. + not_available = 0, + /// Service is available. + available = 1, +}; + +/// \brief Function type for indicating service state changes to the service user. +using Service_state_change_callback = score::cpp::move_only_function; + +/// \brief Function type for indicating event updates to the service user. +using Event_update_callback = + score::cpp::move_only_function; + +/// \brief Function type for allocating event payloads. +using Event_payload_allocate_callback = + score::cpp::move_only_function>( + Client_connector const&, Event_id)>; + +/// \brief Interface for applications to use a service (client-role). +/// \details Changes of service instance state are indicated by callback on_service_state_change. +/// +/// A Client_connector instance is connected to a Server_connector instance only if the service +/// interfaces are compatible. The compatibility check contains checks for semantic version and +/// service interface members. +/// +/// If the service state is not Service_state::available, service API calls have no effect and +/// return Error::runtime_error_service_not_available. +/// +/// If the passed parameter client_id is not valid (not contained in the client connector or server +/// connector specific Service_interface_definition), service API calls have no effect and +/// return Error::logic_error_id_out_of_range. +/// +/// If the Service_interface_definition of the Client_connector instance contains a member +/// configuration, then the client_id is translated to the server_id based on the matching member +/// names (configuration.members.events and configuration.members.methods). +/// +/// If the Service_interface_definition of the Client_connector instance does not contain a +/// member configuration, then the client_id is translated to the server_id 1:1. +/// +/// The Client_connector callback on_event_update is called if an available Enabled_server_connector +/// calls update_event(). +/// +/// The Client_connector callback on_event_requested_update is called if an available +/// Enabled_server_connector instance calls update_requested_event() and the +/// Client_connector instance previously requested an event update with request_event_update(). +class Client_connector { + public: + /// \brief Alias for an unique pointer to this interface. + using Uptr = std::unique_ptr; + + /// \brief Client_connector callback interface needed at Client_connector construction, see + /// Runtime::make_client_connector(). + /// + /// \attention All user callbacks must not block and shall return quickly (simple algorithms + /// only). No callback is allowed to destroy the Client_connector, otherwise it will result in a + /// deadlock. If a deadlock situation is detected, a warning will be logged and the application + /// terminated. + struct Callbacks { + /// \brief Callback is called on any service state change. + Service_state_change_callback on_service_state_change; + /// \brief Callback is called on a server triggered event update. + Event_update_callback on_event_update; + /// \brief Callback is called on a client requested event update, see + /// Client_connector::subscribe_event() and Client_connector::request_event_update(). + Event_update_callback on_event_requested_update; + /// \brief Callback is called to allocate event payloads. + Event_payload_allocate_callback on_event_payload_allocate; + }; + + /// \brief Constructor. + /// \details A Client_connector instance and a Server_connector instance do not match under the + /// following conditions: + /// - the instance.id of both is different; + /// - the configuration.interface.id of both is different; + /// - the configuration.interface.version.major of both is different; + /// - the Client_connector's configuration.interface.version.minor is larger than the + /// Server_connector's. + /// + /// After construction the service state is always Service_state::not_available. + /// This initial state is not indicated through the callback on_service_state_change(). + /// + /// If the SOCom service registry connects a Client_connector to the matching Server_connector, + /// then the SOCom service registry calls the callback + /// on_service_state_change(Service_state::available, ...). + /// + /// If the SOCom service registry disconnects an available Server_connector from a + /// Client_connector, then the Client_connector calls the callback + /// on_service_state_change(Service_state::not_available, ...). + Client_connector() = default; + + /// \brief Destructor. + /// \details Unregisters from the server connector and destroys the client connector. + /// After destruction no registered callbacks are called any more. + /// + /// Blocks until all operations have completed and the service state is (implicitly) + /// Service_state::not_available. It does not call callback on_service_state_change(). + /// + /// Aborts all method calls. This ensures no method reply will be invoked after completion the + /// destructor. + /// + /// Implicitly unsubscribes all subscribed events. + /// + /// Detect deadlocks, which are caused by destroying the Client_connector from a running + /// Client_connector callback. When a deadlock is detected, the destructor logs and + /// terminates the application. + virtual ~Client_connector() noexcept = default; + + Client_connector(Client_connector const&) = delete; + Client_connector(Client_connector&&) = delete; + + Client_connector& operator=(Client_connector const&) = delete; + Client_connector& operator=(Client_connector&&) = delete; + + /// \brief Allocate a payload for the given method ID. + /// + /// This requires a Server_connector to be connected to which payload allocation is delegated. + /// + /// \param method_id ID of the method for which a payload should be allocated. + /// \return A writable payload in case of successful operation, otherwise an error. + [[nodiscard]] + virtual Result> allocate_method_call_payload( + Method_id method_id) noexcept = 0; + + /// \brief Subscribe an event to receive event updates from the Server_connector. + /// \details The mode value Event_mode::update_and_initial_value supports the field use-case. + /// + /// The user is responsible for calling subscribe_event() again, if the service state + /// transitions to Service_state::available and a subscription is required. + /// + /// If the service state is Service_state::available, then the Enabled_server_connector instance + /// registers this Client_connector as subscribed for event server_id. + /// + /// The available Enabled_server_connector instance combines the mode parameter with modes of + /// other clients subscription's and stores the result. + /// + /// The mode value Event_mode::update_and_initial_value is dominant while mode value + /// Event_mode::update is recessive. + /// + /// If one subscription requests mode value Event_mode::update_and_initial_value, + /// then the resulting stored mode value is Event_mode::update_and_initial_value. + /// + /// All subscriptions are lost if the service state Service_state::available is left. + /// + /// If this is the first subscription for this event server_id at the Enabled_server_connector + /// instance (no matter from which Client_connector), then the Enabled_server_connector instance + /// calls callback on_event_subscription_change(server_id, Event_state::subscribed). + /// + /// If this is the first subscription for this event server_id at the Enabled_server_connector + /// instance and the parameter mode is Event_mode::update_and_initial_value, then the + /// Enabled_server_connector instance stores the Client_connector instance in a list of update + /// requesters for event server_id and calls callback on_event_update_request(server_id) after + /// calling callback on_event_subscription_change(). + /// + /// \param client_id ID of the event. + /// \param mode Mode of the event. + /// \return Void in case of successful operation, otherwise an error. + virtual Result subscribe_event(Event_id client_id, Event_mode mode) const noexcept = 0; + + /// \brief Unsubscribes from an event to stop receiving event updates. + /// \details If the service state is Service_state::available, then the available + /// Enabled_server_connector instance unregisters this Client_connector for event server_id and + /// removes this Client_connector instance from the list of update requesters for event + /// server_id. + /// + /// If this is the last Client_connector instance unsubscribing for a specific event server_id + /// at the Enabled_server_connector instance, then the Enabled_server_connector instance calls + /// callback on_event_subscription_change(server_id, Event_state::not_subscribed). + /// \param client_id ID of the event. + /// \return Void in case of successful operation, otherwise an error. + virtual Result unsubscribe_event(Event_id client_id) const noexcept = 0; + + /// \brief Requests an event update. + /// \details If the service state is Service_state::available, then the available + /// Enabled_server_connector instance stores the Client_connector instance in a list of update + /// requesters for event server_id and calls callback on_event_update_request(server_id) if this + /// is the first update_request for the event. + /// \param client_id ID of the event. + /// \return Void in case of successful operation, otherwise an error. + virtual Result request_event_update(Event_id client_id) const noexcept = 0; + + /// \brief Calls a method at the Server_connector side. + /// \details If reply_data is nullopt, then the Server application (of the + /// Enabled_server_connector instance) and the Method_invocation object returned do not allocate + /// any resources for this method call and callback on_method_reply() will not be called. + /// + /// If reply_data is not nullopt, then the server application (of the + /// Enabled_server_connector instance) returns a Method_invocation object which allocates + /// resources required for the ongoing method invocation. Once the method invocation is + /// completed, the server application calls reply_data.reply_callback(). + /// Discarding the Method_invocation object cancels the method invocation. + /// + /// If the service state is Service_state::available, then the available Server_connector + /// instance calls the callback on_method_call(server_id, payload, reply_data). + /// \param client_id ID of the method. + /// \param payload Payload to be called with. + /// \param reply_data Callback and payload buffer in case a reply is requested. + /// \return A pointer to a Method_invocation object in case of successful invocation, otherwise + /// an error. + [[nodiscard]] virtual Result call_method( + Method_id client_id, Payload::Sptr payload, + Method_call_reply_data_opt reply_data = std::nullopt) const noexcept = 0; + + /// \brief Retrieves the peer posix credentials from the server. + /// \details If the client connector is not connected, then an error is returned. + /// \return Posix credentials in case of successful operation, otherwise an error. + [[nodiscard]] virtual Result get_peer_credentials() const noexcept = 0; + + [[nodiscard]] virtual Service_interface_definition const& get_configuration() + const noexcept = 0; + [[nodiscard]] virtual Service_instance const& get_service_instance() const noexcept = 0; + [[nodiscard]] virtual bool is_service_available() const noexcept = 0; +}; + +} // namespace score::socom + +#endif // SRC_SOCOM_INCLUDE_SCORE_SOCOM_CLIENT_CONNECTOR diff --git a/src/socom/include/score/socom/error.hpp b/src/socom/include/score/socom/error.hpp new file mode 100644 index 00000000..a221f8d1 --- /dev/null +++ b/src/socom/include/score/socom/error.hpp @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_ERROR_HPP +#define SCORE_SOCOM_ERROR_HPP + +#include + +#include "score/result/error_code.h" +#include "score/result/error_domain.h" +#include "score/result/result.h" + +namespace score::socom { + +/// \brief Error conditions when using Client_connector. +enum class Error : score::result::ErrorCode { + /// Service state is not Service_state::available. Service_state::available cannot prevent + /// network issues, so if it is important that the Server receives a method call, it always has + /// to send some return value via the callback. + runtime_error_service_not_available, + /// Request is rejected. + runtime_error_request_rejected, + /// Event or method ID is out of range. + logic_error_id_out_of_range, + /// Payload cannot be deserialized. + runtime_error_malformed_payload, + /// Access is denied. + runtime_error_permission_not_allowed, +}; + +score::result::Error MakeError(Error code, std::string_view user_message = "") noexcept; + +/// \brief Error conditions when using Enabled_server_connector. +enum class Server_connector_error : score::result::ErrorCode { + /// Event or method ID is out of range. + logic_error_id_out_of_range, + runtime_error_no_client_subscribed_for_event, +}; + +score::result::Error MakeError(Server_connector_error code, + std::string_view user_message = "") noexcept; + +/// \brief Errors upon connector construction. +enum class Construction_error : score::result::ErrorCode { + duplicate_service, ///< Service identifier already exists. + callback_missing ///< At least one of the provided callbacks is missing. +}; + +score::result::Error MakeError(Construction_error code, + std::string_view user_message = "") noexcept; + +} // namespace score::socom + +#endif // SCORE_SOCOM_ERROR_HPP diff --git a/src/socom/include/score/socom/event.hpp b/src/socom/include/score/socom/event.hpp new file mode 100644 index 00000000..df0b42e0 --- /dev/null +++ b/src/socom/include/score/socom/event.hpp @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_EVENT_HPP +#define SCORE_SOCOM_EVENT_HPP + +#include + +namespace score::socom { + +/// \brief Alias for an event ID. +using Event_id = std::uint16_t; + +/// \brief Mode of an event. +enum class Event_mode : std::uint8_t { + update = 0U, ///< Without initial value request. + update_and_initial_value ///< With initial value request. +}; + +/// \brief State of an event subscription. +enum class Event_state : std::uint8_t { + /// Enabled_server_connector: There is no Client_connector subscribed to the event. + /// Client_connector: The Enabled_server_connector did not acknowledge or reject the + /// subscription. + unsubscribed, + /// Enabled_server_connector: There is at least one Client_connector subscribed to the event. + /// Client_connector: The Enabled_server_connected acknowledged the subscription. + subscribed +}; + +} // namespace score::socom + +#endif // SCORE_SOCOM_EVENT_HPP diff --git a/src/socom/include/score/socom/method.hpp b/src/socom/include/score/socom/method.hpp new file mode 100644 index 00000000..051f8083 --- /dev/null +++ b/src/socom/include/score/socom/method.hpp @@ -0,0 +1,143 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_METHOD_HPP +#define SCORE_SOCOM_METHOD_HPP + +#include +#include +#include +#include +#include +#include + +namespace score::socom { +/// \brief Alias for a method ID. +using Method_id = std::uint16_t; + +/// \brief A variadic template struct that implements the Visitor pattern. +/// \details The Visitor struct inherits from a parameter pack of types 'Ts' and aggregates their +/// operator() overloads. This allows for seamless type-specific operations on multiple types +/// without modifying their definitions. +/// \tparam Ts Types to be combined into the Visitor. +template +struct Visitor : Ts... { + using Ts::operator()...; +}; + +/// \brief A template deduction guide that simplifies the syntax for creating instances of Visitor +/// by eliminating the need to specify the template arguments. +/// \tparam Ts Types to be deduced from the constructor parameters. +template +Visitor(Ts...) -> Visitor; + +/// \brief Interface class for method call RAII type (see Client_connector::call_method). +class Method_invocation { + public: + /// \brief Alias for an unique pointer to this interface. + using Uptr = std::unique_ptr; + + Method_invocation() = default; + virtual ~Method_invocation() = default; + + Method_invocation(Method_invocation const&) = delete; + Method_invocation(Method_invocation&&) = delete; + + Method_invocation& operator=(Method_invocation const&) = delete; + Method_invocation& operator=(Method_invocation&&) = delete; +}; + +/// \brief Result of successful method call. +struct Application_return { + /// \brief Constructor. + /// \param p Payload data. + explicit Application_return(Payload::Sptr p = empty_payload()) : payload{std::move(p)} {} + + /// \brief Payload data. + Payload::Sptr payload; +}; + +/// \brief Result of failed method call. +struct Application_error { + /// \brief Alias for an error code. + using Code = std::int32_t; + + /// \brief Constructor. + /// \param p Payload data. + explicit Application_error(Payload::Sptr p = empty_payload()) : payload{std::move(p)} {} + + /// \brief Constructor. + /// \param c Error code. + /// \param p Payload data. + explicit Application_error(Code c, Payload::Sptr p = empty_payload()) + : code{c}, payload{std::move(p)} {} + + /// \brief Error code. + Code code{}; + + /// \brief Payload data. + Payload::Sptr payload; +}; + +/// \brief Alias for the response of a method. +using Method_result = std::variant; + +/// \brief Operator == for Application_return. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of equality, otherwise false. +inline bool operator==(Application_return const& lhs, Application_return const& rhs) { + return *lhs.payload == *rhs.payload; +} + +/// \brief Operator != for Application_return. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of inequality, otherwise false. +inline bool operator!=(Application_return const& lhs, Application_return const& rhs) { + return !(lhs == rhs); +} + +/// \brief Operator == for Application_error. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of equality, otherwise false. +inline bool operator==(Application_error const& lhs, Application_error const& rhs) { + return (lhs.code == rhs.code) && (*lhs.payload == *rhs.payload); +} + +/// \brief Operator != for Application_error. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of inequality, otherwise false. +inline bool operator!=(Application_error const& lhs, Application_error const& rhs) { + return !(lhs == rhs); +} + +/// \brief Alias for the callback function of a method, in case a reply is requested. +using Method_reply_callback = score::cpp::move_only_function; + +/// \brief Callback and payload buffer for method call replies. +struct Method_call_reply_data { + Method_reply_callback reply_callback; + Writable_payload::Uptr reply_payload; + + Method_call_reply_data(Method_reply_callback reply_callback, + Writable_payload::Uptr reply_payload); +}; + +using Method_call_reply_data_opt = std::optional; + +} // namespace score::socom + +#endif // SCORE_SOCOM_METHOD_HPP diff --git a/src/socom/include/score/socom/payload.hpp b/src/socom/include/score/socom/payload.hpp new file mode 100644 index 00000000..fd821fd2 --- /dev/null +++ b/src/socom/include/score/socom/payload.hpp @@ -0,0 +1,107 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_PAYLOAD_HPP +#define SCORE_SOCOM_PAYLOAD_HPP + +#include +#include +#include + +namespace score::socom { + +/// \brief Interface representing the Payload transferable by SOCom. +/// \details The payload itself must be representable by a continuous Span of bytes. +/// +/// The Payload has an optional header(), which is writable, but is not part of the data +/// returned by data(). The optional header() is part of the same internal buffer, which also +/// backs data(). +/// +/// The payload can internally look as follows: +/// xxxxxxx SOME/IP_header | payload_data +/// +/// Here | shows the position of the actual payload start in the buffer. Here "payload_data" +/// will be returned with data(). +/// +/// This is needed for algorithms like the one for E2E, which require all data +/// to be in contiguous memory and require an additional header for processing. +/// \note When sending data over the wire, only data returned by data() shall be sent. +class Payload { + public: + /// \brief Alias for a shared pointer to this interface. + using Sptr = std::shared_ptr; + + /// \brief Alias for a data byte. + using Byte = std::byte; + + /// \brief Alias for payload data. + using Span = score::cpp::span; + + /// \brief Alias for writable payload data. + using Writable_span = score::cpp::span; + + Payload() = default; + virtual ~Payload() = default; + Payload(Payload const&) = delete; + Payload(Payload&&) = delete; + Payload& operator=(Payload const&) = delete; + Payload& operator=(Payload&&) = delete; + + /// \brief Retrieves the payload data. + /// \return Span of payload data. + [[nodiscard]] virtual Span data() const noexcept = 0; + + /// \brief Retrieves the header data. + /// \return Span of header data. + [[nodiscard]] virtual Span header() const noexcept = 0; + + /// \brief Retrieves the header data. + /// \return Writable span of header data. + [[nodiscard]] virtual Writable_span header() noexcept = 0; +}; + +/// \brief An empty payload instance, which may be used as default value for the payload parameter. +/// \return A pointer to a Payload object. +extern Payload::Sptr empty_payload(); + +/// \brief Operator == for Payload. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of equality, otherwise false. +bool operator==(Payload const& lhs, Payload const& rhs); + +/// \brief Operator != for Payload. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of inequality, otherwise false. +bool operator!=(Payload const& lhs, Payload const& rhs); + +/// \brief Interface representing a writable payload, which can be allocated by the recipient for +/// zero copy operations. +/// +/// The recipient is responsible for allocating enough data for the sender. +class Writable_payload : public Payload { + public: + /// \brief Alias for a shared pointer to this interface. + using Sptr = std::shared_ptr; + /// \brief Alias for a unique pointer to this interface. + using Uptr = std::unique_ptr; + + /// \brief Retrieves the writable payload data. + /// \return Span of payload data. + [[nodiscard]] virtual Writable_span wdata() noexcept = 0; +}; + +} // namespace score::socom + +#endif // SCORE_SOCOM_PAYLOAD_HPP diff --git a/src/socom/include/score/socom/posix_credentials.hpp b/src/socom/include/score/socom/posix_credentials.hpp new file mode 100644 index 00000000..c64bc606 --- /dev/null +++ b/src/socom/include/score/socom/posix_credentials.hpp @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_POSIX_CREDENTIALS_HPP +#define SCORE_SOCOM_POSIX_CREDENTIALS_HPP + +#include +namespace score::socom { + +/// \brief Posix_credentials. +struct Posix_credentials final { + /// \brief user ID + ::uid_t uid; + /// \brief group ID + ::gid_t gid; +}; + +} // namespace score::socom + +#endif // SCORE_SOCOM_POSIX_CREDENTIALS_HPP diff --git a/src/socom/include/score/socom/registry_string_view.hpp b/src/socom/include/score/socom/registry_string_view.hpp new file mode 100644 index 00000000..ed7c795d --- /dev/null +++ b/src/socom/include/score/socom/registry_string_view.hpp @@ -0,0 +1,198 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SRC_SOCOM_INCLUDE_SCORE_SOCOM_REGISTRY_STRING_VIEW +#define SRC_SOCOM_INCLUDE_SCORE_SOCOM_REGISTRY_STRING_VIEW + +#include +#include + +namespace score::socom { + +/// +/// \class Registry_string_view +/// +/// \brief Registry_string_view is an unmodifiable view of a string held in String_registry. +/// For performance reasons the comparison between instances of Registry_string_view is done +/// using simple pointer equality check and a check against other String_views is done by +/// character comparison. +/// It is the programmer's responsibility to ensure that Registry_string_view does not +/// outlive the String_registry it is referring to. +/// +class Registry_string_view final { + friend class String_registry; + + public: + using const_pointer = typename std::string_view::const_pointer; + using const_iterator = typename std::string_view::const_iterator; + using difference_type = typename std::string_view::difference_type; + using size_type = typename std::string_view::size_type; + // npos is a constant representing the largest possible value of size_type, because of + // signed-to-unsigned implicit conversion. It is used as an "end of string" or "not found" + // indicator. More details: https://en.cppreference.com/w/cpp/string/basic_string/npos + static constexpr size_type npos = static_cast(-1); + + /// + /// \brief Copy constructor (default) + /// + constexpr Registry_string_view(Registry_string_view const& other) noexcept = default; + + /// + /// \brief Move constructor (default) + /// + constexpr Registry_string_view(Registry_string_view&& other) noexcept = default; + + /// + /// \brief Destructor (default) + /// + ~Registry_string_view() noexcept = default; + + /// + /// \brief Copy assignment operator (default) + /// + constexpr Registry_string_view& operator=(Registry_string_view const& view) & noexcept = + default; + + /// + /// \brief Move assignment operator (default) + /// + constexpr Registry_string_view& operator=(Registry_string_view&& view) & noexcept = default; + + /// + /// \brief Get string data + /// + constexpr const_pointer data() const noexcept { return m_string_view.data(); } + + /// + /// \brief Get string length + /// + constexpr size_type length() const noexcept { return m_string_view.length(); } + + /// + /// \brief Get string length + /// + constexpr size_type size() const noexcept { return m_string_view.size(); } + + /// + /// \brief Get whether the string is empty + /// + constexpr bool empty() const noexcept { return m_string_view.empty(); } + + /// + /// \brief Const iterator to the beginning + /// + constexpr const_iterator begin() const noexcept { return data(); } + + /// + /// \brief Const iterator to the beginning + /// + constexpr const_iterator cbegin() const noexcept { return data(); } + + /// + /// \brief Iterator to end + /// + const_iterator end() const noexcept { + return std::next(data(), static_cast(length())); + } + + /// + /// \brief Const iterator to the end + /// + const_iterator cend() const noexcept { + return std::next(data(), static_cast(length())); + } + + constexpr std::string_view string_view() const noexcept { return m_string_view; } + + private: + std::string_view m_string_view; + + explicit constexpr Registry_string_view(std::string_view view) + : m_string_view{std::move(view)} {} +}; + +/// +/// \brief operator== +/// +constexpr bool operator==(Registry_string_view lhs, Registry_string_view rhs) noexcept { + return (lhs.data() == rhs.data()) && (lhs.length() == rhs.length()); +} + +/// +/// \brief operator!= +/// +constexpr bool operator!=(Registry_string_view lhs, Registry_string_view rhs) noexcept { + return !(lhs == rhs); +} + +/// +/// \brief operator< +/// +constexpr bool operator<(Registry_string_view lhs, Registry_string_view rhs) noexcept { + return lhs.string_view() < rhs.string_view(); +} + +/// +/// \brief operator<= +/// +constexpr bool operator<=(Registry_string_view lhs, Registry_string_view rhs) noexcept { + return lhs.string_view() <= rhs.string_view(); +} + +/// +/// \brief operator> +/// +constexpr bool operator>(Registry_string_view lhs, Registry_string_view rhs) noexcept { + return lhs.string_view() > rhs.string_view(); +} + +/// +/// \brief operator>= +/// +constexpr bool operator>=(Registry_string_view lhs, Registry_string_view rhs) noexcept { + return lhs.string_view() >= rhs.string_view(); +} + +/// +/// \brief operator<< +/// Overload of << to write the string from a Registry_string_view to ostream. +/// +/// \return Reference to the ostream object used for writing, to enable chaining of << operations +/// as usual. +/// +inline std::ostream& operator<<(std::ostream& os, Registry_string_view v) { + return os.write(v.data(), static_cast(v.length())); +} + +} // namespace score::socom + +namespace std { + +/// \brief std::hash specialization for Registry_string_view +/// +/// \return Hash value for the given Registry_string_view +/// +template <> +struct hash<::score::socom::Registry_string_view> { + size_t operator()(::score::socom::Registry_string_view const& sv) const noexcept { + // For the conversion of Registry_string_view data pointer to intptr_t, the reinterpret_cast + // is necessary. + // Pointer-to-integer conversion is required for memory address hashing in this case. + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return std::hash{}(reinterpret_cast(sv.data())); + } +}; + +} // namespace std + +#endif // SRC_SOCOM_INCLUDE_SCORE_SOCOM_REGISTRY_STRING_VIEW diff --git a/src/socom/include/score/socom/runtime.hpp b/src/socom/include/score/socom/runtime.hpp new file mode 100644 index 00000000..9adba5f8 --- /dev/null +++ b/src/socom/include/score/socom/runtime.hpp @@ -0,0 +1,331 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_RUNTIME_HPP +#define SCORE_SOCOM_RUNTIME_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace score::socom { + +/// \brief Service bridge identification. +class Bridge_identity { + public: + /// \brief Creates instance of Bridge_identity. + /// \param instance Reference instance. + /// \tparam T Type of instance. + /// \return Bridge_identity object. + template + static Bridge_identity make(T const& instance) { + return Bridge_identity{static_cast(&instance)}; + } + + /// \brief Operator == for Bridge_identity. + /// \param lhs Bridge_identity to compare. + /// \param rhs Bridge_identity to compare. + /// \return True in case of equality, otherwise false. + friend bool operator==(Bridge_identity lhs, Bridge_identity rhs) noexcept { + return lhs.m_identity == rhs.m_identity; + } + + private: + explicit Bridge_identity(void const* identity) : m_identity{identity} {} + + void const* m_identity; +}; + +/// \brief Operator != for Bridge_identity. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of inequality, otherwise false. +inline bool operator!=(Bridge_identity const& lhs, Bridge_identity const& rhs) { + return !(lhs == rhs); +} + +/// \brief Interface class for Find_subscription RAII type (see Runtime). +class Find_subscription_handle { + public: + Find_subscription_handle() = default; + virtual ~Find_subscription_handle() = default; + + Find_subscription_handle(Find_subscription_handle const&) = delete; + Find_subscription_handle(Find_subscription_handle&&) = delete; + + Find_subscription_handle& operator=(Find_subscription_handle const&) = delete; + Find_subscription_handle& operator=(Find_subscription_handle&&) = delete; +}; + +/// \brief Interface class for Service_bridge_registration RAII type (see Runtime). +class Service_bridge_registration_handle { + public: + Service_bridge_registration_handle() = default; + virtual ~Service_bridge_registration_handle() = default; + + Service_bridge_registration_handle(Service_bridge_registration_handle const&) = delete; + Service_bridge_registration_handle(Service_bridge_registration_handle&&) = delete; + + Service_bridge_registration_handle& operator=(Service_bridge_registration_handle const&) = + delete; + Service_bridge_registration_handle& operator=(Service_bridge_registration_handle&&) = delete; + + /// \brief Getter for Bridge_identity. + /// \return Bridge_identity object. + [[nodiscard]] virtual Bridge_identity get_identity() const = 0; +}; + +/// \brief Interface class for Service_request RAII type (see Runtime). +class Service_request_handle { + public: + Service_request_handle() = default; + virtual ~Service_request_handle() = default; + + Service_request_handle(Service_request_handle const&) = delete; + Service_request_handle(Service_request_handle&&) = delete; + + Service_request_handle& operator=(Service_request_handle const&) = delete; + Service_request_handle& operator=(Service_request_handle&&) = delete; +}; + +/// \brief RAII object that represents an active find service subscription, see +/// Runtime::subscribe_find_service(). +using Find_subscription = std::unique_ptr; +/// \brief RAII object that represents a service bridge registration at the runtime, see +/// Runtime::register_service_bridge() +using Service_bridge_registration = std::unique_ptr; +/// \brief RAII object that represents an service request from the runtime to a service bridge, see +/// Runtime::register_service_bridge(). +using Service_request = std::unique_ptr; + +/// \brief [[deprecated]] Find service result type, see Runtime::subscribe_find_service(). +using Find_result_container = std::vector; + +/// \brief Status of reported service. +enum class Find_result_status : std::uint8_t { + added, ///< A new service is found. + deleted ///< A service is removed. +}; + +/// \brief [[deprecated]] Find service result indication callback type, see +/// Runtime::subscribe_find_service(). +using Find_result_callback = std::function; + +/// \brief Find service result indication callback type, see Runtime::subscribe_find_service(). +using Find_result_change_callback = std::function; + +/// \brief Subscribe_find_service interface type signature. +using Subscribe_find_service_function = std::function)>; + +/// \brief Request_service interface type signature. +using Request_service_function = + std::function; + +/// \brief Interface that provides access to the service oriented communication (SOCom) middleware. +/// \details SOCom implements a client-service-server based architectural pattern. +/// A service is an instance (Service_instance) of an interface (Service_interface). +/// A server provides a service. +/// Clients use services. +/// The service pattern makes client and server independent from concrete instances of each other +/// (loose coupling). Depending on their availability, SOCom performs the dependency resolution +/// client/server connection and disconnection at runtime. + +/// A service interface supports the following communication patterns: +/// - method call (1:1) +/// - client-server-client +/// - client-server +/// - event, also known as publish/subscribe (1:n) +/// - server-clients +class Runtime { + public: + /// \brief Alias for an unique pointer to this interface. + using Uptr = std::unique_ptr; + + Runtime() = default; + virtual ~Runtime() noexcept = default; + Runtime(Runtime const&) = delete; + Runtime(Runtime&&) = delete; + Runtime& operator=(Runtime const&) = delete; + Runtime& operator=(Runtime&&) = delete; + + /// \brief Creates a new client connector. + /// \details Returns a new instance Client_connector registered as a service user for the + /// service defined by the configuration.interface and instance parameters to the SOCom service + /// registry. + /// + /// If the first client connector for [configuration, instance] is created and the requested + /// service is locally not present, make_client_connector() calls + /// request_service(configuration, instance) on every bridge (already registered or registered + /// later) and stores the Service_request RAII objects. + /// + /// If the last client connector for [configuration, instance] is destroyed, + /// make_client_connector() deletes all associated service bridge Service_request RAII objects. + /// \param configuration Service interface configuration. + /// \param instance Service instance. + /// \param callbacks User callbacks to be called based on the internal states. + /// \return A pointer to a Client_connector instance in case of successful operation, otherwise + /// an error. + /// \note Construction_error::callback_missing is returned if any of the callbacks is not set. + /// \note This method sets the values returned from getuid() and getpid() (unistd.h) as + /// credentials of the returned Client_connector. + [[nodiscard]] + virtual Result make_client_connector( + Service_interface_definition configuration, Service_instance instance, + Client_connector::Callbacks callbacks) noexcept = 0; + + /// \brief Creates a new client connector. + /// \details This method behaves the same as the make_client_connector() above. + /// Additionally, custom posix credentials can be passed. + /// \param configuration Service interface configuration. + /// \param instance Service instance. + /// \param callbacks User callbacks to be called based on the internal states. + /// \param credentials Posix credentials to be set for the client connector. + /// \return A pointer to a Client_connector instance in case of successful operation, otherwise + /// an error. + /// \note Construction_error::callback_missing is returned if any of the callbacks is not set. + [[nodiscard]] + virtual Result make_client_connector( + Service_interface_definition configuration, Service_instance instance, + Client_connector::Callbacks callbacks, Posix_credentials const& credentials) noexcept = 0; + + /// \brief Creates a new server connector. + /// \details Returns a new instance of Disabled_server_connector registered as a service + /// provider for the service defined by the configuration.interface and instance parameters to + /// the SOCom service registry if this service does not exist in the runtime service registry + /// yet. + /// + /// Logs an error and returns Construction_error::duplicate_service if a service defined by the + /// configuration.interface and instance parameters is already registered in the runtime service + /// registry. + + /// Returns Construction_error::callback_missing if any of the callbacks is not set. + /// \param configuration Service interface configuration. + /// \param instance Service instance. + /// \param callbacks User callbacks to be called based on the internal states. + /// \return A pointer to a server connector instance in case of successful operation, otherwise + /// an error. + /// \note This method sets the values returned from getuid() and getpid() (unistd.h) as + /// credentials of the returned Disabled_server_connector. + [[nodiscard]] + virtual Result make_server_connector( + Server_service_interface_definition configuration, Service_instance instance, + Disabled_server_connector::Callbacks callbacks) noexcept = 0; + + /// \brief Creates a new server connector. + /// \details This method behaves the same as the make_server_connector() above. + /// Additionally, custom posix credentials can be passed. + /// \param configuration Service interface configuration. + /// \param instance Service instance. + /// \param callbacks User callbacks to be called based on the internal states. + /// \param credentials Posix credentials to be set for the server connector. + /// \return A pointer to a server connector instance in case of successful operation, otherwise + /// an error. + [[nodiscard]] + virtual Result make_server_connector( + Server_service_interface_definition configuration, Service_instance instance, + Disabled_server_connector::Callbacks callbacks, + Posix_credentials const& credentials) noexcept = 0; + + /// \brief Offers the same functionality as the subscribe_find_service() below. + /// \details The complete list of currently available services is passed into the callback on + /// every change. + /// + /// If the set of known services matching the parameters interface and instance changes compared + /// to the last invocation of callback on_result_set, on_result_set is called with the complete + /// list of currently available services. + /// \param on_result_set_change Callback function. + /// \param interface Service interface. + /// \param instance Optional service instance. + /// \return Object that represents an active find service subscription. + [[nodiscard]] [[deprecated( + "Removed due to complexity. Use Client_connectors Service_state_change_callback instead.")]] + virtual Find_subscription subscribe_find_service( + Find_result_callback on_result_set_change, Service_interface_identifier const& interface, + std::optional instance) noexcept = 0; + + /// \brief Calls on_result_change when a new service is found or a service is removed. + /// \note Interface and instance are used to filter for specific services. + /// \details Immediately reports the all currently known service instances matching the given + /// interface and instance to the callback on_result_change and returns a RAII object + /// representing this find subscription. + /// + /// If the set of known services matching the parameters interface and instance changes compared + /// to the last invocation of the callback on_result_change, on_result_change is called + /// with the new set of known service instances. + /// + /// If the object representing a find subscription is released, then any further changes are no + /// longer indicated through the callback on_result_change. + /// + /// If the parameter instance has no value, all instances matching the interface are part of the + /// result. + /// + /// If the callback on_result_change is nullptr, find subscription is not performed and the + /// callback on_result_change is never called. + /// + /// The method subscribe_find_service(interface, instance) is called on every already registered + /// or later registered bridge and the Service_request RAII objects are stored. + /// + /// If the last find service subscription for [interface, instance] is destroyed, + /// subscribe_find_service() deletes all associated find service subscription RAII objects. + /// + /// A service bridge contributes to the result-set of find service subscriptions by calling the + /// Find_result_change_callback for the specific bridge which is part of the + /// subscribe_find_service interface. Thus changes on the set of known service instances must be + /// indicated as locally created services. Duplicate services indicate a system configuration + /// error. + /// + /// If the one and only find service subscriber is a bridge that indicates the existence of the + /// parameter identity, then no find service request forwarding to the respective bridge + /// is active. + /// \param on_result_change Callback function. + /// \param interface Service interface. + /// \param instance Service instance. + /// \param identity Optional bridge identity. + /// \return Object that represents an active find service subscription. + [[nodiscard]] [[deprecated( + "Removed due to complexity. Use Client_connectors Service_state_change_callback instead.")]] + virtual Find_subscription subscribe_find_service( + Find_result_change_callback on_result_change, + std::optional interface, + std::optional instance, + std::optional identity) noexcept = 0; + + /// \brief Registers a bridge which transports events or method calls over an IPC channel. + /// \param identity Bridge identity. + /// \param subscribe_find_service Function to call in order to search for services. + /// \param request_service Function to call if the requested service is not present locally. + /// \return A registration RAII object in case of successful operation, otherwise an error. + /// \note Construction_error::callback_missing is returned if any of the callbacks is not set. + [[nodiscard]] + virtual Result register_service_bridge( + Bridge_identity identity, Subscribe_find_service_function subscribe_find_service, + Request_service_function request_service) noexcept = 0; +}; + +/// \brief Function to instantiate a Runtime object. +/// \param logger Logger for logging messages. +/// \return Pointer to Runtime object. +Runtime::Uptr create_runtime(); + +} // namespace score::socom + +#endif // SCORE_SOCOM_RUNTIME_HPP diff --git a/src/socom/include/score/socom/server_connector.hpp b/src/socom/include/score/socom/server_connector.hpp new file mode 100644 index 00000000..88c5e892 --- /dev/null +++ b/src/socom/include/score/socom/server_connector.hpp @@ -0,0 +1,227 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_SERVER_CONNECTOR_HPP +#define SCORE_SOCOM_SERVER_CONNECTOR_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace score::socom { + +class Disabled_server_connector; +class Enabled_server_connector; + +/// \brief Function type for indicating an event subscription state change to the service provider. +using Event_subscription_change_callback = + score::cpp::move_only_function; + +/// \brief Function type for indicating an event update request to the service provider. +using Event_request_update_callback = + score::cpp::move_only_function; + +/// \brief Function type for processing any client side method invocation. +using Method_call_credentials_callback = score::cpp::move_only_function; + +/// \brief Function type for indicating a method call payload request to the service provider. +using Method_call_payload_allocate_callback = + score::cpp::move_only_function(Enabled_server_connector&, + Method_id)>; + +class Configuration_getter { + public: + virtual ~Configuration_getter() = default; + + [[nodiscard]] + virtual Server_service_interface_definition const& get_configuration() const noexcept = 0; + [[nodiscard]] + virtual Service_instance const& get_service_instance() const noexcept = 0; +}; + +/// \brief Interface for applications to use a service (server-role). +/// \details This interface represents a Server_connector not visible to any Client_connector(s). +/// After destruction no registered callbacks are called anymore. +/// All user callbacks must not block and shall return quickly (simple algorithms only). +class Disabled_server_connector : public Configuration_getter { + public: + /// \brief Alias for an unique pointer to this interface. + using Uptr = std::unique_ptr; + + Disabled_server_connector() = default; + virtual ~Disabled_server_connector() noexcept = default; + + Disabled_server_connector(Disabled_server_connector const&) = delete; + Disabled_server_connector(Disabled_server_connector&&) = delete; + + Disabled_server_connector& operator=(Disabled_server_connector const&) = delete; + Disabled_server_connector& operator=(Disabled_server_connector&&) = delete; + + /// \brief Server_Connector callback interface needed at Server_connector construction, see + /// Runtime::make_server_connector(). + /// + /// \details All user callbacks must not block and shall return quickly (simple algorithms + /// only). No callback is allowed to destroy the Server_connector, otherwise it will result in a + /// deadlock. If a deadlock situation is detected, a warning will be logged and the application + /// terminated. + struct Callbacks { + /// \brief Callback is called on any client side method invocation. + Method_call_credentials_callback on_method_call; + + /// \brief Callback is called if an event is subscribed by the first Client_connector or + /// unsubscribed by the last Client_connector. + Event_subscription_change_callback on_event_subscription_change; + + /// \brief Callback is called if an event update is requested by any Client_connector. + /// \details On a call to callback on_event_update_request(), the Server application calls + /// update_requested_event() or update_event() for the requested event as follows: + /// - update_requested_event() is called if no new data is available from the + /// application (indicate current state only to requesting clients); + /// - update_event() is called if new data is available from the application (indicate new + /// state to all clients). + Event_request_update_callback on_event_update_request; + + /// \brief Callback is called to allocate method call payloads. + Method_call_payload_allocate_callback on_method_call_payload_allocate; + }; + + /// \brief Makes the service available to clients. + /// \details Changes the connector to state 'Enabled' and converts it to an + /// Enabled_server_connector. Registers the Enabled_server_connector at the SOCom service + /// registry, connects each matching registered Client_connector to this instance and calls the + /// callback on_service_state_change(Service_state::available, server_configuration) of each + /// connected Client_connector instance. + /// + /// Server_connector instance callbacks may be called after entering enable(). + /// \param connector Disabled server connector. + /// \return An enabled server connector. + [[nodiscard]] + static std::unique_ptr enable( + std::unique_ptr connector); + + protected: + /// \cond INTERNAL + virtual Enabled_server_connector* enable() = 0; + /// \endcond +}; + +/// \brief Interface for applications to use a service (server-role). +/// \details This interface represents an enabled Server_connector, thus it is registered by the +/// SOCom service registry and available to connected Client_connector(s). +/// +/// If a client calls Client_connector::call_method, then the callback on_method_called() is called. +/// +/// If the client-aggregated need of an event changes, then callback on_event_subscription_change() +/// is called. +/// +/// If a client requests the current value of an event, then callback on_event_update_request() is +/// called. +/// +/// If the passed parameter server_id is not valid (not contained in +/// Server_service_interface_definition), service API calls have no effect and return +/// Server_connector_error::logic_error_id_out_of_range. +class Enabled_server_connector : public Configuration_getter { + public: + /// \brief Alias for an unique pointer to this interface. + using Uptr = std::unique_ptr; + + /// \brief Constructor. + Enabled_server_connector() = default; + + /// \brief Destructor. + /// \details Disconnects from Client_connectors and destroys the Server_connector. After + /// destruction no registered callbacks are called anymore. + /// + /// Implicitly calls disable() and deallocates the instance resources. + /// + /// Detects deadlocks which are caused by destroying the Client_connector from a running + /// Client_connector callback. When a deadlock is detected, the destructor shall log and + /// terminate the application. + virtual ~Enabled_server_connector() noexcept = default; + + Enabled_server_connector(Enabled_server_connector const&) = delete; + Enabled_server_connector(Enabled_server_connector&&) = delete; + + Enabled_server_connector& operator=(Enabled_server_connector const&) = delete; + Enabled_server_connector& operator=(Enabled_server_connector&&) = delete; + + /// \brief Removes the connection to the clients. + /// \details Calls the callback on_service_state_change(Service_state::not_available) of + /// each connected Client_connector instances. It disconnects from all connected + /// Client_connector instances and blocks until all clients are disconnected. + /// \param connector Enabled server connector. + /// \return A disabled server connector. + [[nodiscard]] + static std::unique_ptr disable( + std::unique_ptr connector) noexcept; + + /// \brief Allocates a payload for the given event ID. + /// + /// This requires a Client_connector to be subscribed to the event to which payload allocation + /// is delegated. + /// + /// \param event_id ID of the event for which a payload should be allocated. + /// \return A writable payload in case of successful operation, otherwise an error. + [[nodiscard]] + virtual Result> allocate_event_payload( + Event_id event_id) noexcept = 0; + + /// \brief Distributes new event data to all subscribed Client_connectors. + /// \details Clears the list of event update requesters for the event server_id. + /// + /// Calls the callback on_event_update(client_id, payload) for each connected Client_connector + /// which is subscribed to event server_id. + /// \param server_id ID of the event. + /// \param payload Event data. + /// \return Void in case of successful operation, otherwise an error. + virtual Result update_event(Event_id server_id, Payload::Sptr payload) noexcept = 0; + + /// \brief Distributes new event data to all event update requesting Client_connectors. + /// \details Clears the list of event update requesters for the event server_id. + /// + /// Calls the callback on_event_requested_update(client_id, payload) for each connected + /// Client_connector instance in a list of update requesters for event server_id. + /// \param server_id ID of the event. + /// \param payload Event data. + /// \return Void in case of successful operation, otherwise an error. + virtual Result update_requested_event(Event_id server_id, + Payload::Sptr payload) noexcept = 0; + + /// \brief Retrieves the mode of the event server_id. + /// \details Returns the combined event subscription mode for event server_id, see + /// Client_connector::subscribe_event(). + /// + /// Returns Event_mode::update_and_initial_value if any client has subscribed with + /// Event_mode::update_and_initial_value. + /// + /// Returns Event_mode::update if no Client_connector instance has subscribed to this event yet. + /// \param server_id ID of the event. + /// \return An event mode in case of successful operation, otherwise an error. + [[nodiscard]] virtual Result get_event_mode(Event_id server_id) const noexcept = 0; + + protected: + /// \cond INTERNAL + virtual Disabled_server_connector* disable() noexcept = 0; + /// \endcond +}; + +} // namespace score::socom + +#endif // SCORE_SOCOM_SERVER_CONNECTOR_HPP diff --git a/src/socom/include/score/socom/service_interface_definition.hpp b/src/socom/include/score/socom/service_interface_definition.hpp new file mode 100644 index 00000000..61eaddc4 --- /dev/null +++ b/src/socom/include/score/socom/service_interface_definition.hpp @@ -0,0 +1,108 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SRC_SOCOM_INCLUDE_SCORE_SOCOM_SERVICE_INTERFACE_DEFINITION +#define SRC_SOCOM_INCLUDE_SCORE_SOCOM_SERVICE_INTERFACE_DEFINITION + +#include +#include +#include + +namespace score::socom { + +/// description: Strong type for forced proper construction. +enum class Num_of_events : std::size_t {}; + +/// description: Strong type for forced proper construction. +enum class Num_of_methods : std::size_t {}; + +inline Num_of_events to_num_of_events(std::size_t const value) noexcept { + return static_cast(value); +} + +inline Num_of_methods to_num_of_methods(std::size_t const value) noexcept { + return static_cast(value); +} + +/// \brief Service interface configuration data structure for Client_connector instances. +/// \details This type, which is used by Runtime::make_client_connector(), allows an optional member +/// configuration. +struct Service_interface_definition final { + /// \brief Constructor for default use-case. + /// \param sif Service interface identification information. + /// \param methods Methods of the service interface. + /// \param events Events of the service interface. + Service_interface_definition(Service_interface_identifier sif, Num_of_methods num_of_methods, + Num_of_events num_of_events); + + /// \brief Constructor without methods and events. + /// \details Client_connectors which have no member configuration must use the provided + /// Server_service_interface_definition configuration. + /// \param sif Service interface identification information. + explicit Service_interface_definition(Service_interface_identifier sif); + + Service_interface_definition(Service_interface_definition const&) = default; + Service_interface_definition(Service_interface_definition&&) noexcept = default; + + ~Service_interface_definition() noexcept = default; + + Service_interface_definition& operator=(Service_interface_definition const&) = delete; + Service_interface_definition& operator=(Service_interface_definition&&) = delete; + + /// \brief Service interface identification information. + Service_interface_identifier const interface; + std::size_t num_methods{0U}; + std::size_t num_events{0U}; +}; + +bool operator==(Service_interface_definition const& lhs, Service_interface_definition const& rhs); + +bool operator<(Service_interface_definition const& lhs, Service_interface_definition const& rhs); + +/// \brief Service interface configuration data structure for Server_connector instances. +/// \details This type, which is used by Runtime::make_server_connector(), enforces a member +/// configuration. +class Server_service_interface_definition final { + Service_interface_definition m_configuration; + + public: + /// \brief Constructor. + /// \param sif Service interface identification information. + /// \param methods Methods of the service interface. + /// \param events Events of the service interface. + Server_service_interface_definition(Service_interface_identifier const& sif, + Num_of_methods num_of_methods, Num_of_events num_of_events); + + Server_service_interface_definition(Server_service_interface_definition const& rhs); + Server_service_interface_definition(Server_service_interface_definition&& rhs) noexcept; + + ~Server_service_interface_definition() noexcept = default; + + Server_service_interface_definition& operator=(Server_service_interface_definition const&) = + delete; + Server_service_interface_definition& operator=(Server_service_interface_definition&&) = delete; + + // Service_interface_definition + /// \brief Retrieves the configuration by implicitly converting an instance to + /// Service_interface_definition. + /// \return The stored configuration. + operator Service_interface_definition() const; + + std::size_t get_num_methods() const noexcept; + std::size_t get_num_events() const noexcept; + Service_interface_identifier const& get_interface() const noexcept; +}; + +} // namespace score::socom + +#endif // SRC_SOCOM_INCLUDE_SCORE_SOCOM_SERVICE_INTERFACE_DEFINITION diff --git a/src/socom/include/score/socom/service_interface_identifier.hpp b/src/socom/include/score/socom/service_interface_identifier.hpp new file mode 100644 index 00000000..3d1d5901 --- /dev/null +++ b/src/socom/include/score/socom/service_interface_identifier.hpp @@ -0,0 +1,191 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SRC_SOCOM_INCLUDE_SCORE_SOCOM_SERVICE_INTERFACE_IDENTIFIER +#define SRC_SOCOM_INCLUDE_SCORE_SOCOM_SERVICE_INTERFACE_IDENTIFIER + +#include +#include +#include +#include +#include +#include + +namespace score::socom { + +/// Service instance identification information +class Service_instance final { + public: + using Id = Registry_string_view; + + /// String-based service instance identifier. + Id id; + + /// \brief Constructor. + /// \param new_id ID of the service interface. + explicit Service_instance(Id new_id) noexcept : id{new_id} {} + + /// \brief Constructor. + /// \param new_id ID of the service interface. + explicit Service_instance(std::string_view new_id) + : id{score::socom::instance_id_registry().insert(new_id).first} {} + + /// \brief Constructor. + /// \param new_id ID of the service interface. + /// \param is_static_string_literal Tag to indicate that the provided string is a static string + /// literal. + Service_instance(std::string_view new_id, Literal_tag is_static_string_literal) + : id{score::socom::instance_id_registry().insert(new_id, is_static_string_literal).first} {} + + /// \brief Constructor. + /// \param new_id ID of the service interface. + explicit Service_instance(std::string&& new_id) + : id{score::socom::instance_id_registry().insert(std::move(new_id)).first} {} +}; + +/// \brief Operator == for Service_instance. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of equality, otherwise false. +inline bool operator==(Service_instance const& lhs, Service_instance const& rhs) { + return lhs.id == rhs.id; +} + +/// \brief Operator < for Service_instance. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of lhs is less than rhs, otherwise false. +inline bool operator<(Service_instance const& lhs, Service_instance const& rhs) { + return lhs.id < rhs.id; +} + +/// \brief Service interface identification information. +struct Service_interface_identifier { + public: + /// \brief Alias for a service interface identifier. + using Id = Registry_string_view; + + /// \brief Service interface version type. + struct Version { + /// \brief Major version information. + /// \note Major version must match exactly for service interface compatibility. + std::uint16_t major; + /// \brief Minor version information. + /// \note Minor version of Client_connector is less or equal than the minor version of + /// Server_connector for service interface compatibility. + std::uint16_t minor; + }; + + /// \brief Service interface identifier. + Id id; + + /// \brief Service interface version information. + Version version; + + /// \brief Constructor. + /// \param new_id ID of the service interface. + /// \param new_version Version of the service interface. + Service_interface_identifier(Id new_id, Version new_version) noexcept + : id{new_id}, version{new_version} {} + + /// \brief Constructor. + /// \param new_id ID of the service interface. + /// \param new_version Version of the service interface. + Service_interface_identifier(std::string_view new_id, Version new_version) + : id{score::socom::service_id_registry().insert(new_id).first}, version{new_version} {} + + /// \brief Constructor. + /// \param new_id ID of the service interface. + /// \param is_static_string_literal Tag to indicate that the provided string is a static string + /// literal. + /// \param new_version Version of the service interface. + Service_interface_identifier(std::string_view new_id, Literal_tag is_static_string_literal, + Version new_version) + : id{score::socom::service_id_registry().insert(new_id, is_static_string_literal).first}, + version{new_version} {} + + /// \brief Constructor. + /// \param new_id ID of the service interface. + /// \param new_version Version of the service interface. + Service_interface_identifier(std::string&& new_id, Version new_version) + : id{score::socom::service_id_registry().insert(std::move(new_id)).first}, + version{new_version} {} +}; + +/// \brief Operator == for Service_interface_identifier::Version. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of equality, otherwise false. +inline bool operator==(Service_interface_identifier::Version const& lhs, + Service_interface_identifier::Version const& rhs) { + return (std::tie(lhs.major, lhs.minor) == std::tie(rhs.major, rhs.minor)); +} + +/// \brief Operator < for Service_interface_identifier::Version. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case the contents of lhs are lexicographically less than the contents of rhs, +/// otherwise false. +inline bool operator<(Service_interface_identifier::Version const& lhs, + Service_interface_identifier::Version const& rhs) { + return (std::tie(lhs.major, lhs.minor) < std::tie(rhs.major, rhs.minor)); +} + +/// \brief Operator == for Service_interface_identifier. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case of equality, otherwise false. +inline bool operator==(Service_interface_identifier const& lhs, + Service_interface_identifier const& rhs) { + return (std::tie(lhs.id, lhs.version) == std::tie(rhs.id, rhs.version)); +} + +/// \brief Operator < for Service_interface_identifier. +/// \param lhs Left-hand side of operator. +/// \param rhs Right-hand side of operator. +/// \return True in case the contents of lhs are lexicographically less than the contents of rhs, +/// otherwise false. +inline bool operator<(Service_interface_identifier const& lhs, + Service_interface_identifier const& rhs) { + return (std::tie(lhs.id, lhs.version) < std::tie(rhs.id, rhs.version)); +} + +} // namespace score::socom + +/// \brief std::hash specialization for Service_instance +/// +/// \return Hash value for the given Service_instance +/// +template <> +struct std::hash { + std::size_t operator()(score::socom::Service_instance const& s) const noexcept { + return std::hash{}(s.id); + } +}; + +/// \brief std::hash specialization for Service_interface_identifier +/// +/// \return Hash value for the given Service_interface_identifier +/// +template <> +struct std::hash { + std::size_t operator()(score::socom::Service_interface_identifier const& s) const noexcept { + std::size_t const h1 = std::hash{}(s.id); + std::size_t const h2 = std::hash{}(s.version.major); + std::size_t const h3 = std::hash{}(s.version.minor); + auto const hash = h1 ^ (h2 << 1) ^ (h3 << 2); + return hash; + } +}; + +#endif // SRC_SOCOM_INCLUDE_SCORE_SOCOM_SERVICE_INTERFACE_IDENTIFIER diff --git a/src/socom/include/score/socom/string_registry.hpp b/src/socom/include/score/socom/string_registry.hpp new file mode 100644 index 00000000..de13ba81 --- /dev/null +++ b/src/socom/include/score/socom/string_registry.hpp @@ -0,0 +1,85 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SRC_SOCOM_INCLUDE_SCORE_SOCOM_STRING_REGISTRY +#define SRC_SOCOM_INCLUDE_SCORE_SOCOM_STRING_REGISTRY + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace score::socom { + +/// \brief Tag to select StringView literal version +struct Literal_tag {}; + +/// +/// \class String_registry +/// +/// \brief A central registry for strings to avoid copying and to facilitate cheap comparison. +/// +class String_registry final { + public: + /// + /// \brief Insert a new StringView literal into the string registry. + /// + /// \param[in] new_string String to be added to the registry. + /// \param[in] (no name) Used only to select StringView literal version of the + /// method. + /// + /// \return String_view of the string in registry and boolean denoting whether the string was + /// newly added (true) or was present already (false). + /// + std::pair insert(std::string_view new_string, + Literal_tag /*is_static_string_literal*/) noexcept; + + /// + /// \brief Insert a new StringView into the string registry. + /// + /// \param[in] new_string String to be added to the registry. + /// + /// \return String_view of the string in registry and boolean denoting whether the string was + /// newly added (true) or was present already (false). + /// + // NOLINTNEXTLINE(bugprone-exception-escape): If exception is thrown it shall be considered as + // fatal error and std::terminate is desired behavior. + std::pair insert(std::string_view new_string) noexcept; + + /// + /// \brief Insert a new new std::string into the string registry. + /// + /// \param[in] new_string String to be added to the registry. + /// + /// \return String_view of the string in registry and boolean denoting whether the string was + /// newly added (true) or was present already (false). + /// + std::pair insert(std::string&& new_string) noexcept; + + private: + std::unordered_set m_registered_strings; + std::forward_list m_dynamic_allocated; + std::mutex m_mutex; +}; + +String_registry& service_id_registry() noexcept; + +String_registry& instance_id_registry() noexcept; + +} // namespace score::socom + +#endif // SRC_SOCOM_INCLUDE_SCORE_SOCOM_STRING_REGISTRY diff --git a/src/socom/include/score/socom/vector_payload.hpp b/src/socom/include/score/socom/vector_payload.hpp new file mode 100644 index 00000000..11315ff8 --- /dev/null +++ b/src/socom/include/score/socom/vector_payload.hpp @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_VECTOR_PAYLOAD_HPP +#define SCORE_SOCOM_VECTOR_PAYLOAD_HPP + +#include +#include +#include + +namespace score::socom { + +/// \brief Alias for payload data. +using Vector_buffer = std::vector; + +/// \brief Creates a Vector_buffer from a list of unsigned integral type elements. +/// \param args List of unsigned integral type elements to be included in the Vector_buffer. +/// \return A Vector_buffer containing the provided elements. +template +Vector_buffer make_vector_buffer(Ts... args) noexcept { + static_assert((std::is_unsigned_v && ...), + "All arguments must be unsigned integral types."); + // TODO check that the types are not larger than Payload::Byte + return {static_cast(args)...}; +} + +/// \brief Creates a vector payload by moving the given data. +/// \param buffer Payload data. +/// \return A pointer to a Payload object. +Payload::Sptr make_vector_payload(Vector_buffer buffer); + +/// \brief Creates a vector payload by moving the given data. +/// \param header_size Size of header data. +/// \param buffer Payload data. +/// \return A pointer to a Payload object. +Payload::Sptr make_vector_payload(std::size_t header_size, Vector_buffer buffer); + +Payload::Sptr make_vector_payload(std::size_t lead_offset, std::size_t header_size, + Vector_buffer buffer); + +/// \brief Creates vector payload from a container. +/// \param container Reference container. +/// \tparam C Container type. +/// \return A pointer to a Payload object. +template +inline Payload::Sptr make_vector_payload(C const& container) { + return make_vector_payload(Vector_buffer{std::begin(container), std::end(container)}); +} + +} // namespace score::socom + +#endif // SCORE_SOCOM_VECTOR_PAYLOAD_FACTORY_HPP diff --git a/src/socom/mock/score/socom/callback_mocks.hpp b/src/socom/mock/score/socom/callback_mocks.hpp new file mode 100644 index 00000000..5082b329 --- /dev/null +++ b/src/socom/mock/score/socom/callback_mocks.hpp @@ -0,0 +1,57 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_CALLBACK_MOCKS_HPP +#define SCORE_SOCOM_CALLBACK_MOCKS_HPP + +#include + +#include +#include +#include +#include + +namespace score::socom { + +// Runtime callbacks +using Find_result_change_callback_mock = ::testing::MockFunction; +using Legacy_find_result_callback_mock = ::testing::MockFunction; + +// Client_connector callbacks +using Service_state_change_callback_mock = Move_only_function_mock; +using Event_update_callback_mock = Move_only_function_mock; +using Event_payload_allocate_callback_mock = + Move_only_function_mock; + +// Server_connector callbacks +using Event_subscription_change_callback_mock = + Move_only_function_mock; +using Event_request_update_callback_mock = Move_only_function_mock; +using Method_call_credentials_callback_mock = + Move_only_function_mock; +using Method_call_payload_allocate_callback_mock = + Move_only_function_mock; + +// Method callbacks +using Method_call_credentials_callback_mock = + Move_only_function_mock; +using Method_reply_callback_mock = Move_only_function_mock; + +// Bridge callbacks +using Subscribe_find_service_function_mock = + ::testing::MockFunction; +using Request_service_function_mock = ::testing::MockFunction; + +} // namespace score::socom + +#endif // SCORE_SOCOM_CALLBACK_MOCKS_HPP diff --git a/src/socom/mock/score/socom/client_connector_mock.hpp b/src/socom/mock/score/socom/client_connector_mock.hpp new file mode 100644 index 00000000..aef4b163 --- /dev/null +++ b/src/socom/mock/score/socom/client_connector_mock.hpp @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_CLIENT_CONNECTOR_MOCK_HPP +#define SCORE_SOCOM_CLIENT_CONNECTOR_MOCK_HPP + +#include + +#include + +namespace score::socom { + +class Client_connector_mock : public Client_connector { + public: + MOCK_METHOD(Result, subscribe_event, (Event_id client_id, Event_mode mode), + (const, noexcept, override)); + MOCK_METHOD(Result, unsubscribe_event, (Event_id), (const, noexcept, override)); + MOCK_METHOD(Result, request_event_update, (Event_id), (const, noexcept, override)); + MOCK_METHOD(Result, call_method, + (Method_id, Payload::Sptr, Method_call_reply_data_opt), + (const, noexcept, override)); + MOCK_METHOD(Result>, allocate_method_call_payload, + (Method_id method_id), (noexcept, override)); + MOCK_METHOD(Result, get_peer_credentials, (), (const, noexcept, override)); + MOCK_METHOD(Service_interface_definition const&, get_configuration, (), + (const, noexcept, override)); + MOCK_METHOD(Service_instance const&, get_service_instance, (), (const, noexcept, override)); + MOCK_METHOD(bool, is_service_available, (), (const, noexcept, override)); +}; + +} // namespace score::socom + +#endif // SCORE_SOCOM_CLIENT_CONNECTOR_MOCK_HPP diff --git a/src/socom/mock/score/socom/move_only_function_mock.hpp b/src/socom/mock/score/socom/move_only_function_mock.hpp new file mode 100644 index 00000000..b4c88795 --- /dev/null +++ b/src/socom/mock/score/socom/move_only_function_mock.hpp @@ -0,0 +1,78 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_MOVE_ONLY_FUNCTION_MOCK_HPP +#define SCORE_SOCOM_MOVE_ONLY_FUNCTION_MOCK_HPP + +#include + +#include +#include +#include +#include +#include + +namespace score::socom { + +namespace internal { + +template +class Move_only_function_mock_base : public ::testing::MockFunction { + public: + using Base = ::testing::MockFunction; + + using Base::Call; + + template + Function_t as_function_impl() { + return [this](Args... args) -> Return_type { + if constexpr (std::is_void_v) { + this->Call(std::forward(args)...); + } else { + return this->Call(std::forward(args)...); + } + }; + } + + std::function AsStdFunction() { + return as_function_impl>(); + } + + Function as_function() { return as_function_impl(); } +}; + +} // namespace internal + +template +class Move_only_function_mock; + +template +class Move_only_function_mock + : public internal::Move_only_function_mock_base, + Return_type, Args...> {}; + +template +class Move_only_function_mock> + : public internal::Move_only_function_mock_base, + Return_type, Args...> {}; + +template +class Move_only_function_mock< + ::score::cpp::move_only_function> + : public internal::Move_only_function_mock_base< + ::score::cpp::move_only_function, + Return_type, Args...> {}; + +} // namespace score::socom + +#endif // SCORE_SOCOM_MOVE_ONLY_FUNCTION_MOCK_HPP diff --git a/src/socom/mock/score/socom/payload_mock.hpp b/src/socom/mock/score/socom/payload_mock.hpp new file mode 100644 index 00000000..0163452a --- /dev/null +++ b/src/socom/mock/score/socom/payload_mock.hpp @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_PAYLOAD_MOCK_HPP +#define SCORE_SOCOM_PAYLOAD_MOCK_HPP + +#include + +#include + +namespace score::socom { + +class Payload_mock : public Payload { + public: + MOCK_METHOD(Span, data, (), (const, noexcept, override)); + MOCK_METHOD(Span, header, (), (const, noexcept, override)); + MOCK_METHOD(Writable_span, header, (), (noexcept, override)); +}; + +class Writable_payload_mock : public score::socom::Writable_payload { + public: + MOCK_METHOD(Span, data, (), (const, noexcept, override)); + MOCK_METHOD(Span, header, (), (const, noexcept, override)); + MOCK_METHOD(Writable_span, header, (), (noexcept, override)); + MOCK_METHOD(Writable_span, wdata, (), (noexcept, override)); +}; + +} // namespace score::socom + +#endif // SCORE_SOCOM_PAYLOAD_MOCK_HPP diff --git a/src/socom/mock/score/socom/runtime_mock.hpp b/src/socom/mock/score/socom/runtime_mock.hpp new file mode 100644 index 00000000..60db69ea --- /dev/null +++ b/src/socom/mock/score/socom/runtime_mock.hpp @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_RUNTIME_MOCK_HPP +#define SCORE_SOCOM_RUNTIME_MOCK_HPP + +#include + +#include + +namespace score::socom { + +class Service_bridge_registration_handle_mock : public Service_bridge_registration_handle { + public: + // mock interface + MOCK_METHOD(Bridge_identity, get_identity, (), (const, override)); +}; + +class Runtime_mock : public Runtime { + public: + // mock interface + MOCK_METHOD(Result, make_client_connector, + (Service_interface_definition, Service_instance, Client_connector::Callbacks), + (noexcept, override)); + MOCK_METHOD(Result, make_client_connector, + (Service_interface_definition, Service_instance, Client_connector::Callbacks, + Posix_credentials const&), + (noexcept, override)); + MOCK_METHOD((Result), make_server_connector, + (Server_service_interface_definition, Service_instance, + Disabled_server_connector::Callbacks), + (noexcept, override)); + MOCK_METHOD((Result), make_server_connector, + (Server_service_interface_definition, Service_instance, + Disabled_server_connector::Callbacks, Posix_credentials const&), + (noexcept, override)); + MOCK_METHOD(Find_subscription, subscribe_find_service, + (Find_result_callback, Service_interface_identifier const&, + std::optional), + (noexcept, override)); + MOCK_METHOD(Find_subscription, subscribe_find_service, + (Find_result_change_callback, std::optional, + std::optional, std::optional), + (noexcept, override)); + MOCK_METHOD(Result, register_service_bridge, + (Bridge_identity, Subscribe_find_service_function, Request_service_function), + (noexcept, override)); +}; + +} // namespace score::socom + +#endif // SCORE_SOCOM_RUNTIME_MOCK_HPP diff --git a/src/socom/mock/score/socom/server_connector_mock.hpp b/src/socom/mock/score/socom/server_connector_mock.hpp new file mode 100644 index 00000000..3e624779 --- /dev/null +++ b/src/socom/mock/score/socom/server_connector_mock.hpp @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_SERVER_CONNECTOR_MOCK_HPP +#define SCORE_SOCOM_SERVER_CONNECTOR_MOCK_HPP + +#include + +#include + +namespace score::socom { + +class Server_connector_mock : public Disabled_server_connector, public Enabled_server_connector { + public: + MOCK_METHOD(Enabled_server_connector*, enable, (), (noexcept, override)); + MOCK_METHOD(Disabled_server_connector*, disable, (), (noexcept, override)); + MOCK_METHOD(Result, update_event, (Event_id, Payload::Sptr), (noexcept, override)); + MOCK_METHOD(Result, update_requested_event, (Event_id, Payload::Sptr), + (noexcept, override)); + MOCK_METHOD(Result, get_event_mode, (Event_id), (const, noexcept, override)); + + MOCK_METHOD(Result>, allocate_event_payload, + (Event_id event_id), (noexcept, override)); + + MOCK_METHOD(Server_service_interface_definition const&, get_configuration, (), + (const, noexcept, override)); + MOCK_METHOD(Service_instance const&, get_service_instance, (), (const, noexcept, override)); +}; + +} // namespace score::socom + +#endif // SCORE_SOCOM_SERVER_CONNECTOR_MOCK_HPP diff --git a/src/socom/src/client_connector_impl.cpp b/src/socom/src/client_connector_impl.cpp new file mode 100644 index 00000000..ab42aa8b --- /dev/null +++ b/src/socom/src/client_connector_impl.cpp @@ -0,0 +1,240 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "client_connector_impl.hpp" + +#include +#include +#include + +#include "messages.hpp" +#include "runtime_impl.hpp" +#include "score/socom/client_connector.hpp" +#include "server_connector_impl.hpp" + +namespace score { +namespace socom { +namespace client_connector { + +Impl::Impl(Runtime_impl& runtime, Service_interface_definition configuration, + Service_instance instance, Client_connector::Callbacks callbacks, + Posix_credentials const& credentials) + : m_configuration{std::move(configuration)}, + m_instance{std::move(instance)}, + m_callbacks{std::move(callbacks)}, + m_stop_block_token{ + std::make_shared([this]() { m_stop_complete_promise.set_value(); })}, + m_registration{runtime.register_connector(m_configuration, m_instance, + make_on_server_update_callback())}, + m_credentials{credentials} { + assert(m_registration); +} + +Impl::~Impl() noexcept { + { + std::lock_guard const lock{m_mutex}; + m_stop_block_token.reset(); + m_registration.reset(); + m_server.reset(); + } +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + + // death tests cannot contribute to code coverage + auto const log_on_deadlock = [this]() { + // destruction from within callback detected + std::cerr << "SOCom error: A callback causes the Client_connector instance to be destroyed " + "by which the callback is called. This leads to a deadlock because the " + "destructor waits until all callbacks are done.: interface=" + << m_configuration.interface.id << std::endl; + }; + + m_deadlock_detector.check_deadlock(log_on_deadlock); +#endif + auto const wait_for_stop_complete = [this]() { m_stop_complete_promise.get_future().wait(); }; + Final_action const catch_promise_exceptions{wait_for_stop_complete}; +} + +message::Subscribe_event::Return_type Impl::subscribe_event(Event_id client_id, + Event_mode mode) const noexcept { + return send(message::Subscribe_event{client_id, mode}); +} + +message::Unsubscribe_event::Return_type Impl::unsubscribe_event(Event_id client_id) const noexcept { + return send(message::Unsubscribe_event{client_id}); +} + +message::Request_event_update::Return_type Impl::request_event_update( + Event_id client_id) const noexcept { + return send(message::Request_event_update{client_id}); +} + +message::Call_method::Return_type Impl::call_method( + Method_id client_id, Payload::Sptr payload, + Method_call_reply_data_opt reply_data) const noexcept { + Method_call_reply_data_opt internal_reply_data; + + if (reply_data) { + struct Context { +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + Impl const* self; +#endif + Method_reply_callback reply_callback; + Weak_reference_token weak_stop_block_token; + + Context( +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + Impl const* self, +#endif + Method_reply_callback reply_callback, Weak_reference_token weak_stop_block_token) + : +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + self{self}, +#endif + reply_callback{std::move(reply_callback)}, + weak_stop_block_token{std::move(weak_stop_block_token)} { + } + }; + + auto context = std::make_unique( +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + this, +#endif + std::move(reply_data->reply_callback), create_weak_block_token()); + + auto wrapped_reply_callback = [context = + std::move(context)](Method_result const& method_reply) { + auto const stop_block_token = context->weak_stop_block_token.lock(); + if (stop_block_token) { +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + Temporary_thread_id_add const tmptia{ + context->self->m_deadlock_detector.enter_callback()}; +#endif + (context->reply_callback)(method_reply); + } + }; + + internal_reply_data.emplace(Method_call_reply_data{std::move(wrapped_reply_callback), + std::move(reply_data->reply_payload)}); + } + + return send( + message::Call_method{client_id, payload, std::move(internal_reply_data), m_credentials}); +} + +Result> Impl::allocate_method_call_payload( + Method_id method_id) noexcept { + return send(message::Allocate_method_call_payload{method_id}); +} + +Result Impl::get_peer_credentials() const noexcept { + return send(message::Posix_credentials{}); +} + +Service_interface_definition const& Impl::get_configuration() const noexcept { + return m_configuration; +} + +Service_instance const& Impl::get_service_instance() const noexcept { return m_instance; } + +bool Impl::is_service_available() const noexcept { return m_server.has_value(); } + +message::Service_state_change::Return_type Impl::receive(message::Service_state_change message) { + if (message.state == Service_state::not_available) { + std::lock_guard const lock{m_mutex}; + m_server.reset(); + } +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + Temporary_thread_id_add const tmptia{m_deadlock_detector.enter_callback()}; +#endif + m_callbacks.on_service_state_change(*this, message.state, message.configuration); +} + +message::Update_event::Return_type Impl::receive(message::Update_event message) { +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + Temporary_thread_id_add const tmptia{m_deadlock_detector.enter_callback()}; +#endif + m_callbacks.on_event_update(*this, message.id, message.payload); +} + +message::Update_requested_event::Return_type Impl::receive( + message::Update_requested_event message) { +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + Temporary_thread_id_add const tmptia{m_deadlock_detector.enter_callback()}; +#endif + m_callbacks.on_event_requested_update(*this, message.id, message.payload); +} + +message::Allocate_event_payload::Return_type Impl::receive( + message::Allocate_event_payload message) { +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + Temporary_thread_id_add const tmptia{m_deadlock_detector.enter_callback()}; +#endif + return m_callbacks.on_event_payload_allocate(*this, message.id); +} + +Impl::Server_indication Impl::make_on_server_update_callback() { + return [this, weak_stop_token = create_weak_block_token()]( + Server_connector_listen_endpoint const& listen_endpoint) { + auto const locked_token = weak_stop_token.lock(); + // Destroying client-connector before this callback runs is not possible with + // deterministic results. + + if (nullptr == locked_token) { + // Client_connector destruction detected + return; + } + + auto endpoint = Client_connector_endpoint(*this, locked_token); + auto const connect_return = listen_endpoint.send(message::Connect{endpoint}); + // Endpoint not accessible for testing to inject determinstic error-condition + + if (!connect_return) { + return; + } + + // As the false condition happens on the non deterministic behavior of thread scheduling + // it cannot be tested reliably in unit tests and is therefore excluded in the coverage. + + if (set_id_mappings_and_server(*connect_return)) { + receive(connect_return->service_state); + } + }; +} + +bool Impl::set_id_mappings_and_server(message::Connect_return const& connect_return) { + std::lock_guard const lock{m_mutex}; + // The dtor could have been started by another thread while this callback is active. + // If the dtor is active setting m_server will lead to a deadlock. Thus check if the + // dtor is running by checking m_stop_block_token for nullptr. + // As this happens on the non deterministic behavior of thread scheduling it cannot + // be tested reliably in unit tests and is therefore excluded in the coverage. + // Only Bullseye is excluded as it may happen that the following condition is never + // false. + + if (nullptr == m_stop_block_token) { + return false; + } + + m_server = connect_return.endpoint; + return true; +} + +Weak_reference_token Impl::create_weak_block_token() const { + std::lock_guard const lock{m_mutex}; + return Weak_reference_token{m_stop_block_token}; +} + +} // namespace client_connector + +} // namespace socom +} // namespace score diff --git a/src/socom/src/client_connector_impl.hpp b/src/socom/src/client_connector_impl.hpp new file mode 100644 index 00000000..5dabd07e --- /dev/null +++ b/src/socom/src/client_connector_impl.hpp @@ -0,0 +1,127 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_CLIENT_CONNECTOR_IMPL_HPP +#define SCORE_SOCOM_CLIENT_CONNECTOR_IMPL_HPP + +#include +#include +#include +#include +#include +#include + +#include "endpoint.hpp" +#include "messages.hpp" +#include "runtime_registration.hpp" +#include "temporary_thread_id_add.hpp" + +namespace score { +namespace socom { + +class Runtime_impl; + +namespace client_connector { +// deadlock detection. +class Impl final : public Client_connector { + public: + using Endpoint = Client_connector_endpoint; + + using Server_indication = + std::function; + + Impl(Runtime_impl& runtime, Service_interface_definition configuration, + Service_instance instance, Client_connector::Callbacks callbacks, + Posix_credentials const& credentials); + Impl(Impl const&) = delete; + Impl(Impl&&) = delete; + Impl& operator=(Impl const&) = delete; + Impl& operator=(Impl&&) = delete; + + ~Impl() noexcept override; + + // interface ::score::socom::Client_connector + Result> allocate_method_call_payload( + Method_id method_id) noexcept override; + message::Subscribe_event::Return_type subscribe_event(Event_id client_id, + Event_mode mode) const noexcept override; + message::Unsubscribe_event::Return_type unsubscribe_event( + Event_id client_id) const noexcept override; + message::Request_event_update::Return_type request_event_update( + Event_id client_id) const noexcept override; + message::Call_method::Return_type call_method( + Method_id client_id, Payload::Sptr payload, + Method_call_reply_data_opt reply_data) const noexcept override; + Result get_peer_credentials() const noexcept override; + Service_interface_definition const& get_configuration() const noexcept override; + Service_instance const& get_service_instance() const noexcept override; + bool is_service_available() const noexcept override; + + // Endpoint API + message::Service_state_change::Return_type receive(message::Service_state_change message); + message::Update_event::Return_type receive(message::Update_event message); + message::Update_requested_event::Return_type receive(message::Update_requested_event message); + message::Allocate_event_payload::Return_type receive(message::Allocate_event_payload message); + + private: + template + ReturnType lock_server(F const& on_server_locked) const; + Server_indication make_on_server_update_callback(); + + bool set_id_mappings_and_server(message::Connect_return const& connect_return); + + Weak_reference_token create_weak_block_token() const; + + // Endpoint APIs + template + typename MessageType::Return_type send(MessageType message) const; + + Service_interface_definition const m_configuration; + Service_instance const m_instance; + Client_connector::Callbacks const m_callbacks; +#ifdef WITH_SOCOM_DEADLOCK_DETECTION + mutable Deadlock_detector m_deadlock_detector; +#endif + mutable std::mutex m_mutex; + std::promise m_stop_complete_promise; + Reference_token m_stop_block_token; // Protected by m_mutex + std::optional m_server; // Protected by m_mutex + Registration m_registration; // Protected by m_mutex + Posix_credentials m_credentials; +}; + +template +ReturnType Impl::lock_server(F const& on_server_locked) const { + std::unique_lock lock{m_mutex}; + auto const locked_server = m_server; + lock.unlock(); + + if (locked_server) { + return on_server_locked(*locked_server); + } + return MakeUnexpected(Error::runtime_error_service_not_available); +} + +template +typename MessageType::Return_type Impl::send(MessageType message) const { + return lock_server( + [&message](Server_connector_endpoint const& server) { + return server.send(std::move(message)); + }); +} + +} // namespace client_connector +} // namespace socom +} // namespace score + +#endif // SCORE_SOCOM_CLIENT_CONNECTOR_IMPL_HPP diff --git a/src/socom/src/endpoint.hpp b/src/socom/src/endpoint.hpp new file mode 100644 index 00000000..1750c9b9 --- /dev/null +++ b/src/socom/src/endpoint.hpp @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_ENDPOINT_HPP +#define SCORE_SOCOM_ENDPOINT_HPP + +#include + +#include "final_action.hpp" + +namespace score { +namespace socom { + +using Reference_token = std::shared_ptr; +using Weak_reference_token = std::weak_ptr; + +template +class Endpoint { + public: + explicit Endpoint(T& connector, Reference_token reference_token) + : m_connector{&connector}, m_reference_token{std::move(reference_token)} {} + + template + inline typename MessageType::Return_type send(MessageType message) const { + return m_connector->receive(std::move(message)); + } + + private: + T* m_connector; + Reference_token m_reference_token; +}; + +namespace client_connector { +class Impl; +} // namespace client_connector + +namespace server_connector { +class Impl; +class Client_connection; +} // namespace server_connector + +using Server_connector_endpoint = Endpoint<::score::socom::server_connector::Client_connection>; +using Client_connector_endpoint = Endpoint<::score::socom::client_connector::Impl>; + +using Server_connector_listen_endpoint = Endpoint<::score::socom::server_connector::Impl>; + +} // namespace socom +} // namespace score + +#endif // SCORE_SOCOM_ENDPOINT_HPP diff --git a/src/socom/src/error.cpp b/src/socom/src/error.cpp new file mode 100644 index 00000000..bb9026f9 --- /dev/null +++ b/src/socom/src/error.cpp @@ -0,0 +1,85 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include + +namespace score::socom { +namespace { + +class Error_error_domain final : public score::result::ErrorDomain { + public: + std::string_view MessageFor(score::result::ErrorCode const& code) const noexcept override { + switch (static_cast(code)) { + case Error::runtime_error_service_not_available: + return "Service not available"; + case Error::runtime_error_request_rejected: + return "Request rejected"; + case Error::logic_error_id_out_of_range: + return "ID out of range"; + case Error::runtime_error_malformed_payload: + return "Malformed payload"; + case Error::runtime_error_permission_not_allowed: + return "Permission not allowed"; + default: + return "Unknown Error"; + } + } +}; + +class Server_connector_error_domain final : public score::result::ErrorDomain { + public: + std::string_view MessageFor(score::result::ErrorCode const& code) const noexcept override { + switch (static_cast(code)) { + case Server_connector_error::logic_error_id_out_of_range: + return "ID out of range"; + case Server_connector_error::runtime_error_no_client_subscribed_for_event: + return "No client subscribed for event"; + default: + return "Unknown Error"; + } + } +}; + +class Construction_error_domain final : public score::result::ErrorDomain { + public: + std::string_view MessageFor(score::result::ErrorCode const& code) const noexcept override { + switch (static_cast(code)) { + case Construction_error::duplicate_service: + return "Duplicate service"; + case Construction_error::callback_missing: + return "Callback missing"; + default: + return "Unknown Error"; + } + } +}; + +} // namespace + +score::result::Error MakeError(Error code, std::string_view user_message) noexcept { + static constexpr Error_error_domain error_domain; + return {static_cast(code), error_domain, user_message}; +} + +score::result::Error MakeError(Server_connector_error code, + std::string_view user_message) noexcept { + static constexpr Server_connector_error_domain error_domain; + return {static_cast(code), error_domain, user_message}; +} + +score::result::Error MakeError(Construction_error code, std::string_view user_message) noexcept { + static constexpr Construction_error_domain error_domain; + return {static_cast(code), error_domain, user_message}; +} + +} // namespace score::socom diff --git a/src/socom/src/final_action.cpp b/src/socom/src/final_action.cpp new file mode 100644 index 00000000..b0a3b1e1 --- /dev/null +++ b/src/socom/src/final_action.cpp @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "final_action.hpp" + +namespace score { +namespace socom { + +Final_action::Final_action(F f) noexcept : m_f{std::move(f)} {} + +Final_action::Final_action(Final_action&& other) noexcept : m_f{std::move(other.m_f)} { + // Reset it always to get consistent behavior. + other.m_f = nullptr; +} + +Final_action::~Final_action() noexcept { execute(); } + +void Final_action::execute() noexcept { + F tmp_f = nullptr; + std::swap(tmp_f, m_f); + try { + if (!tmp_f.empty()) { + tmp_f(); + } + } catch (...) { + } +} + +} // namespace socom +} // namespace score diff --git a/src/socom/src/final_action.hpp b/src/socom/src/final_action.hpp new file mode 100644 index 00000000..f9cc6316 --- /dev/null +++ b/src/socom/src/final_action.hpp @@ -0,0 +1,63 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SRC_SOCOM_SRC_FINAL_ACTION +#define SRC_SOCOM_SRC_FINAL_ACTION + +#include + +namespace score { +namespace socom { + +/// +/// \class Final_action +/// +/// \brief Wraps a functor that shall be executed only when an instance of this class gets destroyed +/// +class Final_action { + public: + using F = score::cpp::move_only_function; + + /// + /// \brief Constructor + /// \param f functor to be called + /// + explicit Final_action(F f) noexcept; + + /// + /// \brief Move constructor + /// + Final_action(Final_action&& other) noexcept; + + Final_action(Final_action const&) = delete; + Final_action& operator=(Final_action const&) = delete; + Final_action& operator=(Final_action&&) = delete; + + /// + /// \brief Destructor + /// + ~Final_action() noexcept; + + /// + /// \brief Runs the functor and disarms the Final_action. It will destroy the stored functor. + /// + void execute() noexcept; + + private: + F m_f; +}; + +} // namespace socom +} // namespace score + +#endif // SRC_SOCOM_SRC_FINAL_ACTION diff --git a/src/socom/src/messages.hpp b/src/socom/src/messages.hpp new file mode 100644 index 00000000..fa2a4ddb --- /dev/null +++ b/src/socom/src/messages.hpp @@ -0,0 +1,102 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_SOCOM_MESSAGES_HPP +#define SCORE_SOCOM_MESSAGES_HPP + +#include +#include + +#include "endpoint.hpp" +#include "score/socom/client_connector.hpp" + +namespace score { +namespace socom { +namespace message { + +struct Request_disconnect { + using Return_type = void; +}; + +struct Service_state_change { + using Return_type = void; + Service_state const state; + Server_service_interface_definition const& configuration; +}; + +struct Connect_return { + using Return_type = void; + Server_connector_endpoint const endpoint; + Service_state_change const service_state; +}; + +struct Connect { + using Return_type = score::Result; + Client_connector_endpoint& endpoint; +}; + +struct Call_method { + using Return_type = score::Result; + Method_id const id; + Payload::Sptr payload; + Method_call_reply_data_opt reply_data; + Posix_credentials const& credentials; +}; + +struct Posix_credentials { + using Return_type = score::Result<::score::socom::Posix_credentials>; +}; + +struct Subscribe_event { + using Return_type = score::Result; + Event_id const id; + Event_mode const mode; +}; + +struct Unsubscribe_event { + using Return_type = score::Result; + Event_id const id; +}; + +struct Request_event_update { + using Return_type = score::Result; + Event_id const id; +}; + +struct Update_event { + using Return_type = void; + Event_id const id; + Payload::Sptr payload; +}; + +struct Update_requested_event { + using Return_type = void; + Event_id const id; + Payload::Sptr payload; +}; + +struct Allocate_event_payload { + using Return_type = score::Result>; + Event_id const id; +}; + +struct Allocate_method_call_payload { + using Return_type = score::Result>; + Method_id const id; +}; + +} // namespace message +} // namespace socom +} // namespace score + +#endif // SCORE_SOCOM_MESSAGES_HPP diff --git a/src/socom/src/method.cpp b/src/socom/src/method.cpp new file mode 100644 index 00000000..5384e0ab --- /dev/null +++ b/src/socom/src/method.cpp @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/socom/method.hpp" + +#include + +namespace score { +namespace socom { + +Method_call_reply_data ::Method_call_reply_data(Method_reply_callback reply_callback, + Writable_payload::Uptr reply_payload) + : reply_callback(std::move(reply_callback)), reply_payload(std::move(reply_payload)) { + assert(!this->reply_callback.empty()); +} + +} // namespace socom +} // namespace score diff --git a/src/socom/src/payload.cpp b/src/socom/src/payload.cpp new file mode 100644 index 00000000..b6f7991d --- /dev/null +++ b/src/socom/src/payload.cpp @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include +#include + +namespace score::cpp { + +template +bool operator==(span const& lhs, span const& rhs) { + return std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs), std::end(rhs)); +} +} // namespace score::cpp + +namespace score::socom { + +bool operator==(Payload const& lhs, Payload const& rhs) { + return (lhs.header() == rhs.header()) && (lhs.data() == rhs.data()); +} + +bool operator!=(Payload const& lhs, Payload const& rhs) { return !(lhs == rhs); } + +Payload::Sptr empty_payload() { + static auto const empty = make_vector_payload({}); + return empty; +} + +} // namespace score::socom diff --git a/src/socom/src/runtime.cpp b/src/socom/src/runtime.cpp new file mode 100644 index 00000000..0a26322a --- /dev/null +++ b/src/socom/src/runtime.cpp @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/socom/runtime.hpp" + +#include + +#include "runtime_impl.hpp" + +namespace score { +namespace socom { + +Runtime::Uptr create_runtime() { return std::make_unique(); } + +} // namespace socom +} // namespace score diff --git a/src/socom/src/runtime_impl.cpp b/src/socom/src/runtime_impl.cpp new file mode 100644 index 00000000..52cb7669 --- /dev/null +++ b/src/socom/src/runtime_impl.cpp @@ -0,0 +1,1021 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "runtime_impl.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "client_connector_impl.hpp" +#include "final_action.hpp" +#include "score/socom/client_connector.hpp" +#include "score/socom/runtime.hpp" +#include "server_connector_impl.hpp" + +namespace score { +namespace socom { + +namespace { + +bool is_matching_instance(std::optional const& filter, + Service_instance const& instance) { + return !filter || *filter == instance; +} + +template +std::vector get_keys(Map const& map) { + std::vector keys; + keys.reserve(map.size()); + for (auto const& pair : map) { + keys.emplace_back(pair.first); + } + return keys; +} + +std::optional const& any_interface() { + static auto const any = std::optional{}; + return any; +} + +std::optional const& any_instance() { + static auto const any = std::optional{}; + return any; +} + +void get_callbacks_to_notify_helper( + std::vector& result, + Runtime_impl::Service_instance_to_callbacks const& instances_to_callbacks, + std::optional const& instance) { + auto const filter_callbacks = instances_to_callbacks.find(instance); + if (std::end(instances_to_callbacks) != filter_callbacks) { + result.insert(std::end(result), std::begin(filter_callbacks->second), + std::end(filter_callbacks->second)); + } +} + +std::vector get_callbacks_to_notify( + Runtime_impl::Interface_to_instance_to_callbacks const& interface_to_callbacks, + Service_interface_identifier const& interface, Service_instance const& instance, + bool const local) { + std::vector result; + + if (local) { + auto const any_interface_instances_to_callbacks = + interface_to_callbacks.find(any_interface()); + if (std::end(interface_to_callbacks) != any_interface_instances_to_callbacks) { + get_callbacks_to_notify_helper(result, any_interface_instances_to_callbacks->second, + any_instance()); + } + } + + auto const instances_to_callbacks = + interface_to_callbacks.find(std::optional{interface}); + if (std::end(interface_to_callbacks) != instances_to_callbacks) { + get_callbacks_to_notify_helper(result, instances_to_callbacks->second, any_instance()); + get_callbacks_to_notify_helper(result, instances_to_callbacks->second, + std::optional{instance}); + } + + return result; +} + +std::thread::id get_invalid_thread_id() { return {}; } + +void notify_subscribed_callbacks( + Currently_running_subscribe_find_service_report& running_service_report, + std::vector const& callbacks_to_notify, + Service_interface_identifier const& interface, Service_instance const& instance, + Find_result_status const status) { + auto const call_callbacks_to_notify = [&callbacks_to_notify, &interface, &instance, &status]() { + for (auto const& cb : callbacks_to_notify) { + auto const locked_cb = cb.lock(); + if (nullptr != locked_cb) { + (*locked_cb)(interface, instance, status); + } + } + }; + + // check if thread id was already set and matches this_thread::id, if yes Server_connector + // creation was triggered by callback and skip locking + if (std::this_thread::get_id() == running_service_report.data) { + call_callbacks_to_notify(); + } else { + // Serialize Find_result_callback calls to reliably detect callback self deletion + // save and remove current thread id, which is checked in stop_subscription() + std::lock_guard const lock{running_service_report.mutex}; + running_service_report.data = std::this_thread::get_id(); + Final_action const reset_data{ + [&running_service_report]() { running_service_report.data = get_invalid_thread_id(); }}; + + call_callbacks_to_notify(); + } +} + +/// \brief Removes all weak_ptrs from list that are either expired or point to item. +/// +/// \param list List of weak_ptrs to clean up. +/// \param item Item to remove from list. All weak_ptrs that point to this item will be removed. +template +void cleanup(std::list>& list, + std::shared_ptr const& item) noexcept { + auto const equals_cb = [&item](auto const& cb_ref) { + auto const locked_cb = cb_ref.lock(); + + // Defensive programming. The true case for the condition (nullptr == locked_cb) will not + // occur because Runtime_impl implements Stop_subscription which further calls + // stop_subscribe when Find_subscription_handle_impl gets destroyed. This will trigger the + // cleanup function and the Find_result_callback gets removed from the list. + // Find_result_callback can not be destroyed from outside in a test to trigger the true + // case for (nullptr == locked_cb). + + return (nullptr == locked_cb) || (item == locked_cb); + }; + + list.remove_if(equals_cb); +} + +/// \brief Removes key from map if value is empty +/// +/// \param map Map to clean up. +/// \param key Key to remove from map if value is empty. +/// \param value Value associated with the key. +template