From 611a4c38db9dae1099e6ce1604a5f9781357533a Mon Sep 17 00:00:00 2001 From: SimonKozik <244535158+SimonKozik@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:18:02 +0100 Subject: [PATCH 01/27] Saving current status --- MODULE.bazel | 11 + MODULE.bazel.lock | 77 +++++- .../src/daemon/src/control/BUILD | 16 ++ .../daemon/src/control/lm_control_server.cpp | 172 ++++++++++++++ .../daemon/src/control/lm_control_server.hpp | 67 ++++++ score/launch_manager/src/execution_error.h | 16 +- score/launch_manager/src/lm_control/BUILD | 53 +++++ .../lm_control/src/details/ilm_control.cpp | 31 +++ .../src/details/lm_control_impl.cpp | 210 +++++++++++++++++ .../src/details/lm_control_impl.hpp | 76 ++++++ .../lm_control_impl_PollLoop_not_working.cpp | 220 ++++++++++++++++++ ...ol_impl__SetReceiveHandler_not_working.cpp | 210 +++++++++++++++++ .../src/details/lm_control_service.h | 163 +++++++++++++ tests/integration/lm_control/BUILD | 51 ++++ .../lm_control/lm_control_ipc_test.cpp | 127 ++++++++++ .../lm_control/lm_control_ipc_test.py | 78 +++++++ .../lm_control/lm_mw_com_config.json | 76 ++++++ .../lm_control/lm_skeleton_stub.cpp | 68 ++++++ .../lm_control/sm_mw_com_config.json | 74 ++++++ 19 files changed, 1793 insertions(+), 3 deletions(-) create mode 100644 score/launch_manager/src/daemon/src/control/lm_control_server.cpp create mode 100644 score/launch_manager/src/daemon/src/control/lm_control_server.hpp create mode 100644 score/launch_manager/src/lm_control/BUILD create mode 100644 score/launch_manager/src/lm_control/src/details/ilm_control.cpp create mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp create mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp create mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_not_working.cpp create mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_not_working.cpp create mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_service.h create mode 100644 tests/integration/lm_control/BUILD create mode 100644 tests/integration/lm_control/lm_control_ipc_test.cpp create mode 100644 tests/integration/lm_control/lm_control_ipc_test.py create mode 100644 tests/integration/lm_control/lm_mw_com_config.json create mode 100644 tests/integration/lm_control/lm_skeleton_stub.cpp create mode 100644 tests/integration/lm_control/sm_mw_com_config.json diff --git a/MODULE.bazel b/MODULE.bazel index 44cb0f648..cf81bf64a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -176,6 +176,17 @@ oci.pull( use_repo(oci, "debian-test-runtime", "debian-test-runtime_linux_amd64") bazel_dep(name = "score_baselibs", version = "0.2.10") +git_override( + module_name = "score_baselibs", + commit = "2daf5b88111d8f6e7f5516f240d4eacc67ceb361", + remote = "https://github.com/eclipse-score/baselibs.git", +) +bazel_dep(name = "score_communication") +git_override( + module_name = "score_communication", + commit = "bcafda8fff9a90bc9e32bf054ad94fb7a627b318", + remote = "git@github.com:eclipse-score/communication.git", +) # Hedron's Compile Commands Extractor for Bazel # https://github.com/hedronvision/bazel-compile-commands-extractor diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 0df551317..65d45ecb7 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -119,40 +119,90 @@ "https://bcr.bazel.build/modules/bazel_worker_api/0.0.4/source.json": "d353c410d47a8b65d09fa98e83d57ebec257a2c2b9c6e42d6fda1cb25e5464a5", "https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/MODULE.bazel": "82494a01018bb7ef06d4a17ec4cd7a758721f10eb8b6c820a818e70d669500db", "https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/source.json": "a2d30458fd86cf022c2b6331e652526fa08e17573b2f5034a9dbcacdf9c2583c", + "https://bcr.bazel.build/modules/boost.any/1.83.0.bcr.4/MODULE.bazel": "b0dd78068b3d4b509e7d2cbafc498687ac347621e303bec26312f63baee1c97d", + "https://bcr.bazel.build/modules/boost.any/1.83.0.bcr.4/source.json": "0b0e71cc436ae601b4ecacf85c7246866c6d39178af0689906b9b997890f984d", + "https://bcr.bazel.build/modules/boost.array/1.83.0.bcr.4/MODULE.bazel": "419a97f36e8cc9cc75a783707051d51e6eaa88f49fda575811c78c76885f097a", + "https://bcr.bazel.build/modules/boost.array/1.83.0.bcr.4/source.json": "551152a3790532596761d6b8bd5f41589b65d4e617c503dce4b3c3ade1d28a71", "https://bcr.bazel.build/modules/boost.assert/1.83.0.bcr.4/MODULE.bazel": "0cc01302bf8f0098013098098f441b06068e6f4d05a4bb4f75a2133d11239e5b", "https://bcr.bazel.build/modules/boost.assert/1.83.0.bcr.4/source.json": "225cb551191ac7f5fb4eba0476a0d0397405cbb360dd9755bf29b28033415379", + "https://bcr.bazel.build/modules/boost.bind/1.83.0.bcr.4/MODULE.bazel": "12265d54e554bcba38ab4d5d9195a597874e202309fe0102ad24ca02d07948cd", + "https://bcr.bazel.build/modules/boost.bind/1.83.0.bcr.4/source.json": "23c74d95825ce383e74922b2ccd0013f0e122be9c0b5a4b80c5aa7c6e2c99496", + "https://bcr.bazel.build/modules/boost.concept_check/1.83.0.bcr.4/MODULE.bazel": "287e9cb97c2dcc11d3c7a08ac58a75a55d1beadff7795164546aebd8d242b515", + "https://bcr.bazel.build/modules/boost.concept_check/1.83.0.bcr.4/source.json": "6d91113521d7906d61e72896eff03b4979beb8eb11e79769fc847d6a89541dd2", "https://bcr.bazel.build/modules/boost.config/1.83.0.bcr.4/MODULE.bazel": "d6433498d31931ef3ec5e237f9a780bd8dbb110a73cccaac15f99df2ff0795a9", "https://bcr.bazel.build/modules/boost.config/1.83.0.bcr.4/source.json": "0a5b5bf8d2afcf0731fcd452fe69f20d45b0d08ba07e70698cc7ee36f0ce7e9c", "https://bcr.bazel.build/modules/boost.container/1.83.0.bcr.4/MODULE.bazel": "8c2711b962cabb11a91dfa24c8019141d7709e2a7d9ad888c7b7bd1f702565ae", "https://bcr.bazel.build/modules/boost.container/1.83.0.bcr.4/source.json": "246ae4564faf1c08995ac3f6a6886999e197821d0e3b3dd641911ae417657602", "https://bcr.bazel.build/modules/boost.container_hash/1.83.0.bcr.4/MODULE.bazel": "2f8246d0e34f19d3e09ba41022e85775566449f6a133640e65b4d9f1cbf110b8", "https://bcr.bazel.build/modules/boost.container_hash/1.83.0.bcr.4/source.json": "0410e34fef0a3dc16edc0f6a8f49783cba04a891183d76286ba974e4d9806ef0", + "https://bcr.bazel.build/modules/boost.conversion/1.83.0.bcr.4/MODULE.bazel": "b8497ee8fd93305ca26b051b28a7dbab2c0754a7b212a7766dc0b419b8d66eb8", + "https://bcr.bazel.build/modules/boost.conversion/1.83.0.bcr.4/source.json": "5c743efda68a0b7b77f2a87b7d66f55b569f3056959e252e472c992ad23f4668", "https://bcr.bazel.build/modules/boost.core/1.83.0.bcr.4/MODULE.bazel": "145932521c0509dbb268a8f062ba5432f0fe2620e6f82a580dab0eacda79a140", "https://bcr.bazel.build/modules/boost.core/1.83.0.bcr.4/source.json": "d709cff67716e366200ca77680b39e9c98141c7289ddbf207b1c710f4de7bf03", "https://bcr.bazel.build/modules/boost.describe/1.83.0.bcr.4/MODULE.bazel": "e705d8ed1d12aba965bb860b9130d05738854168b28573283a75dfa9d98ebc86", "https://bcr.bazel.build/modules/boost.describe/1.83.0.bcr.4/source.json": "748e4e74723c381d304f888849efd52caf5710f48ceb67b91d4e43fc800ccd81", + "https://bcr.bazel.build/modules/boost.detail/1.83.0.bcr.4/MODULE.bazel": "b0716cedd38e241e44c67cdb0616b16062f40e6d0aee62a675abafe58cf81a94", + "https://bcr.bazel.build/modules/boost.detail/1.83.0.bcr.4/source.json": "786d571fbd8482901b62c7c752ca0047c644989610d4cea733b230b93a6500cd", + "https://bcr.bazel.build/modules/boost.function/1.83.0.bcr.4/MODULE.bazel": "69e1415e6c1de48fd9a75cd0f21edddcb3b5a99aba345304548de8b48c5bc258", + "https://bcr.bazel.build/modules/boost.function/1.83.0.bcr.4/source.json": "a0cabb8ba9338689964c5e467186caabed4dd5426b579269be15d1c5c94a5522", + "https://bcr.bazel.build/modules/boost.function_types/1.83.0.bcr.4/MODULE.bazel": "55088b1ae6083c8c58051d006bce058dcd8d3587bbbbd088fc285211ce23f8c2", + "https://bcr.bazel.build/modules/boost.function_types/1.83.0.bcr.4/source.json": "8c58b4cb6d2b2ce126eafb9a060e7b9b6ad60e9f2d409aa2caca84db2369a8ae", + "https://bcr.bazel.build/modules/boost.functional/1.83.0.bcr.4/MODULE.bazel": "6551ccb5fb2c601ff3bd3ca66d0d10deebe79a4b080f98abb61a92fb7ae68c1a", + "https://bcr.bazel.build/modules/boost.functional/1.83.0.bcr.4/source.json": "ce96d3fd8ec61d2889b35734b9d116278b64f473361f8bdeb9a9f336e514356e", + "https://bcr.bazel.build/modules/boost.fusion/1.83.0.bcr.4/MODULE.bazel": "5c809e2a0b84df52732f1982206da585d5366a78bf030ca5eded541f1d7a61ad", + "https://bcr.bazel.build/modules/boost.fusion/1.83.0.bcr.4/source.json": "efce523d915fad1cd37824843ca6381b1e9866bb7ee88851511d85aa37005713", + "https://bcr.bazel.build/modules/boost.integer/1.83.0.bcr.4/MODULE.bazel": "173ce805a8ac6d3a61e2b8891b4bdf32188d36fbbf73c5a5f3ea0e210e246a1d", + "https://bcr.bazel.build/modules/boost.integer/1.83.0.bcr.4/source.json": "6aedf2334ff54a527699cf78a853916826a15252fccdad70b81bfaa1d49904bf", "https://bcr.bazel.build/modules/boost.interprocess/1.83.0.bcr.4/MODULE.bazel": "b1c4cf58b387c07c10cea0709ad808283c62aa8024bebc9ce7a59248c9980729", "https://bcr.bazel.build/modules/boost.interprocess/1.83.0.bcr.4/source.json": "4f9c40d790c736a619c03b5788b80575fadffbb712fb10944572b420313c5dee", "https://bcr.bazel.build/modules/boost.intrusive/1.83.0.bcr.4/MODULE.bazel": "8cfe7ec4ae0e4c33297df4edf5c22e6498cde0d7a1ecf98720392b829918eb61", "https://bcr.bazel.build/modules/boost.intrusive/1.83.0.bcr.4/source.json": "04475d40cac42ebe96888a0599b2b9b35d4092502710f421cb3dc9ad2c6b8099", + "https://bcr.bazel.build/modules/boost.io/1.83.0.bcr.4/MODULE.bazel": "804ae356aacddd2478af291fa29aa682ad813ede8b43a86fa3444b7663b0a855", + "https://bcr.bazel.build/modules/boost.io/1.83.0.bcr.4/source.json": "f07574feba2522bdbb312e99d2978dc1db8bd0a309a118cc6b02559ddca179cc", + "https://bcr.bazel.build/modules/boost.iterator/1.83.0.bcr.4/MODULE.bazel": "7ebfd731d3bbb7d99556652673344093f46799226da22a1d65ddc005fb8adb94", + "https://bcr.bazel.build/modules/boost.iterator/1.83.0.bcr.4/source.json": "298dfee2164bf529195b83641bae0c929ed42e5b2b9097b14c5627a97f40faa8", + "https://bcr.bazel.build/modules/boost.lexical_cast/1.83.0.bcr.4/MODULE.bazel": "1e497e85f499fc55316baa25938a4e02de0a3764fc0152450975fd4ba60b8bc5", + "https://bcr.bazel.build/modules/boost.lexical_cast/1.83.0.bcr.4/source.json": "2731c6c73680ca65a3a60deed4810dd9c7e7f1dd233c07829a313911f90b7fbb", "https://bcr.bazel.build/modules/boost.move/1.83.0.bcr.4/MODULE.bazel": "ddc12982f87e67597eeede240369cc73d4ba83ec764a752c4a471245834ee060", "https://bcr.bazel.build/modules/boost.move/1.83.0.bcr.4/source.json": "6f9971fd617c8d94e7c4a02c618052edec5ad0f30a1b18b4bf9e995443545330", "https://bcr.bazel.build/modules/boost.mp11/1.83.0.bcr.4/MODULE.bazel": "27fa864c5e1799e514bbe2f0b9439ad29380f5d436ae3b39c29109609d81316e", "https://bcr.bazel.build/modules/boost.mp11/1.83.0.bcr.4/source.json": "8c2924bab093f7d0e44cb2ac55dea985497b9d85d6a246c89377a49d27d452a6", + "https://bcr.bazel.build/modules/boost.mpl/1.83.0.bcr.4/MODULE.bazel": "5ffae433a4da96fe4a80e89b7ea2ed9ae638baa8733542e41c04b51654d8ebb0", + "https://bcr.bazel.build/modules/boost.mpl/1.83.0.bcr.4/source.json": "5486afd07c5aed527e6c090cae630e73fa5e95c57fbcbd3e89b6f76759d88acc", + "https://bcr.bazel.build/modules/boost.numeric_conversion/1.83.0.bcr.4/MODULE.bazel": "2f1e9d5483bc5601908aa8db11b4e6d3aa8ee34821740621763ce7388bfe1e44", + "https://bcr.bazel.build/modules/boost.numeric_conversion/1.83.0.bcr.4/source.json": "59be0456e0abf960844d67774f414a2e6165d666bf13d255b30408dde0306dff", + "https://bcr.bazel.build/modules/boost.optional/1.83.0.bcr.4/MODULE.bazel": "5ec81db11f58442b8794af3e8fd868698772f28ba831e2eb2767e8294c222428", + "https://bcr.bazel.build/modules/boost.optional/1.83.0.bcr.4/source.json": "5f52bc2e02908d07e4ad53cacec91520a745471715070ec659c1b1f81d05f5e3", "https://bcr.bazel.build/modules/boost.predef/1.83.0.bcr.4/MODULE.bazel": "4d842ef8cadb835dcfff0ee90a7ff0b6e8df4774c72e4e8443ca2339083cd58b", "https://bcr.bazel.build/modules/boost.predef/1.83.0.bcr.4/source.json": "4bc4c6af4bcc909b1d4abebba5583d03d5e09447c658fa63fe8fdd8a5b2906e5", "https://bcr.bazel.build/modules/boost.preprocessor/1.83.0.bcr.4/MODULE.bazel": "aee3f5151c6c5738a494c7a2b842d1f80db0e60601cc6c1ef2b74e0c7e548fa6", "https://bcr.bazel.build/modules/boost.preprocessor/1.83.0.bcr.4/source.json": "ce770e480d05b987d953d7e64f80cebdfb5b008a9b9da80355d9e86a3d09bbb7", + "https://bcr.bazel.build/modules/boost.program_options/1.83.0.bcr.4/MODULE.bazel": "2e74f997e3e27a604a9dab3d33393737396953eb213429707c65ac60fdec6990", + "https://bcr.bazel.build/modules/boost.program_options/1.83.0.bcr.4/source.json": "b5bb402252a25d926d66ec1bc3946a8264d786f5f82e90a9268abe82e8220c3a", + "https://bcr.bazel.build/modules/boost.range/1.83.0.bcr.4/MODULE.bazel": "6e3089857dd3bb9b31f9c3fcfda86a0d03b9004dd0b6ee5656daee8ade769b54", + "https://bcr.bazel.build/modules/boost.range/1.83.0.bcr.4/source.json": "cb7c9a5fb9d32d08450f75860e6de2f5e9ef3c3b717b48307bba6c73e68ac779", + "https://bcr.bazel.build/modules/boost.regex/1.83.0.bcr.4/MODULE.bazel": "aa6e90068802f8f695712708bb4bd80b4bfc1affb1079a342e506e3563cb892f", + "https://bcr.bazel.build/modules/boost.regex/1.83.0.bcr.4/source.json": "a3ebf3b988c2db3aafa6b91d7fd4cf6f7ac4544829479f7e656d84540abd501f", + "https://bcr.bazel.build/modules/boost.smart_ptr/1.83.0.bcr.4/MODULE.bazel": "5217115fdeb485e3be40802adf97f16c2cc432e050ea0246733b55aeb3ce0047", + "https://bcr.bazel.build/modules/boost.smart_ptr/1.83.0.bcr.4/source.json": "a953c6e0b526efde316af8f6b38170616c559729790efaa121e42230e014f53b", "https://bcr.bazel.build/modules/boost.static_assert/1.83.0.bcr.4/MODULE.bazel": "55a9d94e82ebdc01857bae5590e83a7afab56fc57e86f345288601992eb5cd8e", "https://bcr.bazel.build/modules/boost.static_assert/1.83.0.bcr.4/source.json": "d14305e5679f008a352f75e8eaa725ab9ff71ec1dd2553cda1b171e39fd20cfe", "https://bcr.bazel.build/modules/boost.throw_exception/1.83.0.bcr.4/MODULE.bazel": "6dc898672c5de822af4b4da0fc6d0ed233d9fa942356c4e1bbcd748916ba0c58", "https://bcr.bazel.build/modules/boost.throw_exception/1.83.0.bcr.4/source.json": "ec9d540d7ca6beaffdc7f0395cd5eea0acda924b3ab306966786b242df74b512", + "https://bcr.bazel.build/modules/boost.tokenizer/1.83.0.bcr.4/MODULE.bazel": "0b63f536a387b2ce44641f29be3057e0746554467a0e9081df08c83036854c4a", + "https://bcr.bazel.build/modules/boost.tokenizer/1.83.0.bcr.4/source.json": "ec29f1a3794b132da76d19fbb741c243c890d5a17950fb9e0b7f6bb618b31959", "https://bcr.bazel.build/modules/boost.tuple/1.83.0.bcr.4/MODULE.bazel": "3a21cf25cd86a03bdc78684f16d5b25e91f075ff95230b849545484a7d4d506b", "https://bcr.bazel.build/modules/boost.tuple/1.83.0.bcr.4/source.json": "e145722cc962e7f4385f554ce6e8f4373bab792691712788fa16befa9082b938", + "https://bcr.bazel.build/modules/boost.type_index/1.83.0.bcr.4/MODULE.bazel": "401befa0b9b5a4ee6f2628d7cb0d7664f6a039d9022b982f36a6672ffedeb14d", + "https://bcr.bazel.build/modules/boost.type_index/1.83.0.bcr.4/source.json": "d6b66135155a4094819cc1393d89e4eace6bdf985c5148d31bc96824471382ef", "https://bcr.bazel.build/modules/boost.type_traits/1.83.0.bcr.4/MODULE.bazel": "ecf03c91b87bc800d9c699718fab225973063fc1975082e155298e5a87e19573", "https://bcr.bazel.build/modules/boost.type_traits/1.83.0.bcr.4/source.json": "107e5bef1443ee75efc197198b45fb01d6460b475e20d1fa0d52af64600c8204", + "https://bcr.bazel.build/modules/boost.typeof/1.83.0.bcr.4/MODULE.bazel": "9d76ccc98ad052f184f15741672840519b9b55d793c649c267944099c9107e61", + "https://bcr.bazel.build/modules/boost.typeof/1.83.0.bcr.4/source.json": "b9ac0d3bfded3f175fd0b58514f2409782672407bee2242938df5829176f891e", "https://bcr.bazel.build/modules/boost.unordered/1.83.0.bcr.4/MODULE.bazel": "122f475aca3eb7af7b3e66c1bc6a0139c95ee3629c3625ec7af7ac646be7fad6", "https://bcr.bazel.build/modules/boost.unordered/1.83.0.bcr.4/source.json": "1afd0e208be668faeea97b63d18ef23232c522e300b454d4a2f164c369dcfb2e", + "https://bcr.bazel.build/modules/boost.utility/1.83.0.bcr.4/MODULE.bazel": "c88f9877366b57c1e05b9e4c498cbf42ab45fc4a44d23d634845793af02651c0", + "https://bcr.bazel.build/modules/boost.utility/1.83.0.bcr.4/source.json": "c291874612c0758f629e5852c2133ede86df3e7c53f9d589aa26bcb0578587e7", "https://bcr.bazel.build/modules/boost.winapi/1.83.0.bcr.4/MODULE.bazel": "3a36763aafd5fef4844b012bacdc104674cc3ee6a1e8b501a6d68e964a7df20e", "https://bcr.bazel.build/modules/boost.winapi/1.83.0.bcr.4/source.json": "d4be5cdf5cfdeae83f70211faa034bc41168c75fd98a560464131c8cfc6365fa", "https://bcr.bazel.build/modules/boost/1.83.0.bcr.4/MODULE.bazel": "0ce94f87f4514f6abd77d7ea9d221cc3432e48957c82d712829b36d53183dd68", @@ -641,23 +691,48 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_worker_api/0.0.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_worker_api/0.0.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_worker_java/0.0.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.any/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.array/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.assert/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.bind/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.concept_check/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.config/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.container/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.container_hash/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.conversion/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.core/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.describe/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.detail/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.function/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.function_types/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.functional/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.fusion/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.integer/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.interprocess/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.intrusive/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.io/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.iterator/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.lexical_cast/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.move/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.mp11/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.mpl/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.numeric_conversion/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.optional/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.predef/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.preprocessor/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.program_options/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.range/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.regex/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.smart_ptr/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.static_assert/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.throw_exception/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.tokenizer/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.tuple/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.type_index/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.type_traits/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.typeof/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.unordered/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.utility/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.winapi/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "not found", @@ -939,8 +1014,6 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_swift/1.16.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_swift/1.18.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_swift/2.1.1/MODULE.bazel": "not found", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_baselibs/0.2.10/MODULE.bazel": "eef4a41f8f8b6d5cf55c1445b340a2e8643a8dc83ad9ff52d0d26fe9a77973ec", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_baselibs/0.2.10/source.json": "543286fb4828ff58404a0ddd034cac3e7937657353997119a3976dbaf913f2cf", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_cpp_toolchains/0.5.4/MODULE.bazel": "5f14457f9b708302db05b5dc3c07bd41ae19b91cabf98a0dbffdca622bd6ac91", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_cpp_toolchains/0.5.4/source.json": "2946be24c6b978aa788c934643aa7ec68aac4a926bea4eaa81d512fd48e9f4a8", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/0.0.3/MODULE.bazel": "14c96e378c08705a46abe0799d6236fe3095c342c34f83f8d1b3f6046ce00651", diff --git a/score/launch_manager/src/daemon/src/control/BUILD b/score/launch_manager/src/daemon/src/control/BUILD index 090ccec7d..fd6f91d3e 100644 --- a/score/launch_manager/src/daemon/src/control/BUILD +++ b/score/launch_manager/src/daemon/src/control/BUILD @@ -12,6 +12,22 @@ # ******************************************************************************* load("@rules_cc//cc:defs.bzl", "cc_library") +cc_library( + name = "lm_control_server", + srcs = ["lm_control_server.cpp"], + hdrs = ["lm_control_server.hpp"], + include_prefix = "score/mw/launch_manager/control", + strip_include_prefix = "/score/launch_manager/src/daemon/src/control", + visibility = [ + "//score/launch_manager/src/daemon:__pkg__", + "//tests:__subpackages__", + ], + deps = [ + "//score/launch_manager/src/daemon/src/common:log", + "//score/launch_manager/src/lm_control:lm_control_service", + ], +) + cc_library( name = "control_client_channel", srcs = [ diff --git a/score/launch_manager/src/daemon/src/control/lm_control_server.cpp b/score/launch_manager/src/daemon/src/control/lm_control_server.cpp new file mode 100644 index 000000000..33ea5e81d --- /dev/null +++ b/score/launch_manager/src/daemon/src/control/lm_control_server.cpp @@ -0,0 +1,172 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#include "lm_control_server.hpp" + +#include +#include +#include +#include + +#include "score/mw/launch_manager/common/log.hpp" + +// DEBUG helpers — remove once IPC delivery is confirmed stable. +#define DBG_SKELETON(msg) std::cout<< "[LmControlServer DEBUG] " << msg << std::endl + +namespace score::mw::launch_manager::control +{ + +namespace +{ +constexpr std::string_view kDefaultInstanceSpecifier = "LaunchManager/StateManager/Instance"; +constexpr const char* kInstanceSpecifierEnvVar = "SCORE_LCM_SKELETON_INSTANCE_SPECIFIER"; +} // namespace + +LmControlServer::LmControlServer(std::string_view instance_specifier) + : instance_specifier_{instance_specifier.empty() + ? (std::getenv(kInstanceSpecifierEnvVar) != nullptr + ? std::getenv(kInstanceSpecifierEnvVar) + : std::string{kDefaultInstanceSpecifier}) + : std::string{instance_specifier}} + , skeleton_{} +{ +} + +LmControlServer::~LmControlServer() noexcept +{ + Shutdown(); +} + +bool LmControlServer::Initialize() +{ + auto specifier_result = score::mw::com::InstanceSpecifier::Create(instance_specifier_); + if (!specifier_result.has_value()) + { + LM_LOG_ERROR() << "LmControlServer: invalid instance specifier: " << instance_specifier_; + return false; + } + + auto create_result = lifecycle::LmControlSkeleton::Create(specifier_result.value()); + if (!create_result.has_value()) + { + LM_LOG_ERROR() << "LmControlServer: failed to create skeleton"; + return false; + } + + skeleton_.emplace(std::move(create_result).value()); + + /* ToDo: + * do we have signature mismatches? + * do we needs this lambda to register handler? + * can we pass this class method to the RegisterHandler() method (or use std function)? */ + + /*skeleton_->activate_run_target.RegisterHandler( + [this](const lifecycle::ActivateRunTargetRequest& request) { + return OnActivateRunTarget(request); + });*/ + skeleton_->activate_run_target.RegisterHandler( + [this](lifecycle::ActivateRunTargetResponse& response, + const lifecycle::ActivateRunTargetRequest& request) { + response = OnActivateRunTarget(request); + }); + + /*skeleton_->get_active_run_target.RegisterHandler( + [this]() { + return OnGetActiveRunTarget(); + });*/ + skeleton_->get_active_run_target.RegisterHandler( + [this](lifecycle::GetActiveRunTargetResponse& response) { + response = OnGetActiveRunTarget(); + }); + + auto offer_result = skeleton_->OfferService(); + if (!offer_result.has_value()) + { + LM_LOG_ERROR() << "LmControlServer: OfferService failed"; + skeleton_.reset(); + return false; + } + + LM_LOG_INFO() << "LmControlServer: offering service on " << instance_specifier_; + return true; +} + +void LmControlServer::Shutdown() +{ + if (skeleton_.has_value()) + { + skeleton_->StopOfferService(); + skeleton_.reset(); + LM_LOG_INFO() << "LmControlServer: service stopped"; + } +} + +lifecycle::ActivateRunTargetResponse LmControlServer::OnActivateRunTarget( + const lifecycle::ActivateRunTargetRequest& request) +{ + LM_LOG_INFO() << "LmControlServer: ActivateRunTarget stub — run_target=" + << request.run_target_name; + DBG_SKELETON("OnActivateRunTarget: run_target='" << request.run_target_name << "'"); + + // Send activation_result from a separate thread after a 1-second delay. + if (skeleton_.has_value()) + { + const auto run_target = request.run_target_name; + std::thread([this, run_target]() { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + if (!skeleton_.has_value()) + { + DBG_SKELETON("activation_result: skeleton gone before delayed Send"); + return; + } + + auto alloc = skeleton_->activation_result.Allocate(); + if (!alloc.has_value()) + { + DBG_SKELETON("activation_result.Allocate() failed — event not sent"); + return; + } + + alloc.value().Get()->activated_run_target = run_target; + alloc.value().Get()->activation_source = + lifecycle::RunTargetActivationSource::kStateManagerRequest; + DBG_SKELETON("activation_result.Send: run_target='" << run_target << "'"); + skeleton_->activation_result.Send(std::move(alloc).value()); + DBG_SKELETON("activation_result.Send: done"); + }).detach(); + } + else + { + DBG_SKELETON("OnActivateRunTarget: skeleton not available — event not sent"); + } + + return lifecycle::ActivateRunTargetResponse{ + lifecycle::RequestStatus::kAccepted, + lifecycle::ExecErrc::kGeneralError // unused when kAccepted + }; +} + +lifecycle::GetActiveRunTargetResponse LmControlServer::OnGetActiveRunTarget() +{ + LM_LOG_DEBUG() << "LmControlServer: GetActiveRunTarget stub"; + DBG_SKELETON("OnGetActiveRunTarget: returning 'Running'"); + + return lifecycle::GetActiveRunTargetResponse{ + lifecycle::QueryStatus::kAvailable, + lifecycle::RunTargetName{"Running"}, + lifecycle::ExecErrc::kGeneralError // unused when kAvailable + }; +} + +} // namespace score::mw::launch_manager::control diff --git a/score/launch_manager/src/daemon/src/control/lm_control_server.hpp b/score/launch_manager/src/daemon/src/control/lm_control_server.hpp new file mode 100644 index 000000000..0b8e825d3 --- /dev/null +++ b/score/launch_manager/src/daemon/src/control/lm_control_server.hpp @@ -0,0 +1,67 @@ +/******************************************************************************** + * 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 SCORE_MW_LAUNCH_MANAGER_CONTROL_LM_CONTROL_SERVER_HPP +#define SCORE_MW_LAUNCH_MANAGER_CONTROL_LM_CONTROL_SERVER_HPP + +#include +#include + +#include "score/mw/lifecycle/lm_control_service.h" + +namespace score::mw::launch_manager::control +{ + +/// @brief Hosts the mw::com LmControl skeleton on the Launch Manager side. +/// +/// Offers the LmControlService over mw::com so that State Managers can call +/// activate_run_target and get_active_run_target via the proxy. +/// +/// @note [transitional] Handlers are stubs — they do not delegate to +/// ProcessGroupManager. The activation result is sent immediately with +/// the requested Run Target echoed back. Replace with real +/// IGraphControl wiring in Stage 3. +class LmControlServer final +{ + public: + /// @param instance_specifier The mw::com instance specifier for the + /// skeleton. Reads SCORE_LCM_SKELETON_INSTANCE_SPECIFIER + /// from the environment if empty. + explicit LmControlServer(std::string_view instance_specifier = {}); + + ~LmControlServer() noexcept; + + LmControlServer(const LmControlServer&) = delete; + LmControlServer& operator=(const LmControlServer&) = delete; + LmControlServer(LmControlServer&&) = delete; + LmControlServer& operator=(LmControlServer&&) = delete; + + /// @brief Create the skeleton, register handlers, and offer the service. + /// @return true on success, false if skeleton creation or OfferService failed. + bool Initialize(); + + /// @brief Stop offering the service and destroy the skeleton. + void Shutdown(); + + private: + lifecycle::ActivateRunTargetResponse OnActivateRunTarget( + const lifecycle::ActivateRunTargetRequest& request); + + lifecycle::GetActiveRunTargetResponse OnGetActiveRunTarget(); + + std::string instance_specifier_; + std::optional skeleton_; +}; + +} // namespace score::mw::launch_manager::control + +#endif // SCORE_MW_LAUNCH_MANAGER_CONTROL_LM_CONTROL_SERVER_HPP diff --git a/score/launch_manager/src/execution_error.h b/score/launch_manager/src/execution_error.h index 2dcd913d8..d65235b7d 100644 --- a/score/launch_manager/src/execution_error.h +++ b/score/launch_manager/src/execution_error.h @@ -39,7 +39,13 @@ enum class ExecErrc : score::result::ErrorCode kInTransitionToSameState = 11, ///< Transition to the requested Process Group state failed because transition to ///< requested state is already in progress kNoTimeStamp = 12, ///< DeterministicClient time stamp information is not available - kCycleOverrun = 13 ///< Deterministic activation cycle time exceeded + kCycleOverrun = 13, ///< Deterministic activation cycle time exceeded + kActivationInProgress = + 14, ///< A Run Target activation is already in progress; no single Run Target is currently active + kCallbackInProgress = 15, ///< A new callback cannot be registered because a previously registered callback is + ///< currently being executed + kRequestQueueIsFull = 16, ///< The activation request queue is full; the request was discarded + kRunTargetDoesntExist = 17 ///< The requested Run Target name does not exist in the current configuration }; class ExecErrorDomain final : public score::result::ErrorDomain @@ -77,6 +83,14 @@ class ExecErrorDomain final : public score::result::ErrorDomain return "DeterministicClient time stamp information is not available"; case ExecErrc::kCycleOverrun: return "Deterministic activation cycle time exceeded"; + case ExecErrc::kActivationInProgress: + return "A Run Target activation is already in progress; no single Run Target is currently active"; + case ExecErrc::kCallbackInProgress: + return "A new callback cannot be registered because the current callback is still executing"; + case ExecErrc::kRequestQueueIsFull: + return "The activation request queue is full; the request was discarded"; + case ExecErrc::kRunTargetDoesntExist: + return "The requested Run Target name does not exist in the current configuration"; default: return "Unknown error"; } diff --git a/score/launch_manager/src/lm_control/BUILD b/score/launch_manager/src/lm_control/BUILD new file mode 100644 index 000000000..00cfbd958 --- /dev/null +++ b/score/launch_manager/src/lm_control/BUILD @@ -0,0 +1,53 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_library") + +cc_library( + name = "lm_control_service", + hdrs = ["src/details/lm_control_service.h"], + include_prefix = "score/mw/lifecycle", + strip_include_prefix = "/score/launch_manager/src/lm_control/src/details", + visibility = ["//score:__subpackages__"], + deps = [ + "//score/launch_manager:error", + "//score/launch_manager/src/lm_control:lm_control", + "@score_communication//score/mw/com", + ], +) + +cc_library( + name = "lm_control", + srcs = [ + "src/details/ilm_control.cpp", + "src/details/lm_control_impl.cpp", + "src/details/lm_control_impl.hpp", + "src/details/lm_control_service.h", + ], + hdrs = [ + "src/fixed_string.hpp", + "src/ilm_control.hpp", + "src/run_target_activation_source.hpp", + ], + include_prefix = "score/mw/lifecycle", + strip_include_prefix = "/score/launch_manager/src/lm_control/src", + visibility = [ + "//score:__subpackages__", + "//tests:__subpackages__", + ], + deps = [ + "//score/launch_manager:error", + "@score_baselibs//score/result", + "@score_communication//score/mw/com", + ], +) + diff --git a/score/launch_manager/src/lm_control/src/details/ilm_control.cpp b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp new file mode 100644 index 000000000..8173bdbfc --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp @@ -0,0 +1,31 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#include "score/mw/lifecycle/ilm_control.hpp" +#include "lm_control_impl.hpp" + +namespace score::mw::lifecycle +{ + +score::Result> ILmControl::Create( + std::string_view instance_specifier) +{ + auto impl = std::make_unique(instance_specifier); + if (!impl->IsConnected()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + return impl; +} + +} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp new file mode 100644 index 000000000..c91b17cc8 --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp @@ -0,0 +1,210 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#include "lm_control_impl.hpp" + +#include +#include + +// DEBUG helpers — remove once IPC delivery is confirmed stable. +#define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl + +namespace score::mw::lifecycle +{ + +LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept + : proxy_{} + , callback_{} + , callback_mutex_{} +{ + auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); + if (!specifier_result.has_value()) + { + DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); + return; + } + const auto specifier = std::move(specifier_result).value(); + + // Poll FindService until the LM skeleton is available or timeout expires. + const auto deadline = std::chrono::steady_clock::now() + kFindServiceTimeout; + while (std::chrono::steady_clock::now() < deadline) + { + auto find_result = LmControlProxy::FindService(specifier); + if (find_result.has_value() && !find_result.value().empty()) + { + auto create_result = LmControlProxy::Create(find_result.value().front()); + if (create_result.has_value()) + { + proxy_.emplace(std::move(create_result).value()); + DBG_PROXY("Proxy created successfully"); + break; + } + else + { + DBG_PROXY("Proxy::Create failed"); + } + } + std::this_thread::sleep_for(kFindServiceInterval); + } + + if (!proxy_.has_value()) + { + DBG_PROXY("FindService timed out — no skeleton found within " << kFindServiceTimeout.count() << "s"); + return; + } + + // Subscribe to activation_result events — max 1 sample buffered. + // DEBUG: verify subscription succeeded and log state — remove once event delivery is confirmed stable. + { + auto subscribe_result = proxy_->activation_result.Subscribe(1U); + if (!subscribe_result.has_value()) + { + DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); + proxy_.reset(); + return; + } + + const auto state = proxy_->activation_result.GetSubscriptionState(); + const char* state_str = + state == score::mw::com::SubscriptionState::kSubscribed ? "kSubscribed" : + state == score::mw::com::SubscriptionState::kNotSubscribed ? "kNotSubscribed" : + state == score::mw::com::SubscriptionState::kSubscriptionPending ? "kSubscriptionPending" : + "unknown"; + // Expected: kSubscribed. kSubscriptionPending means the skeleton has not + // yet acknowledged — samples may arrive late or not at all. + DBG_PROXY("activation_result subscription state after Subscribe: " << state_str); + } + + proxy_->activation_result.SetReceiveHandler([this]() { + OnActivationResult(); + }); + DBG_PROXY("SetReceiveHandler registered"); + + // Warmup: call GetNewSamples once to signal to the skeleton that this proxy + // is actively reading. This triggers the skeleton to establish its reverse + // notification channel (LoLa skeleton→proxy socket) before the first event + // is sent, so the SetReceiveHandler notification arrives reliably. + proxy_->activation_result.GetNewSamples( + [](score::mw::com::SamplePtr) noexcept {}, 1U); + DBG_PROXY("Warmup GetNewSamples called"); +} + +LmControlImpl::~LmControlImpl() noexcept +{ + if (proxy_.has_value()) + { + proxy_->activation_result.Unsubscribe(); + DBG_PROXY("Unsubscribed and destroyed proxy"); + } +} + +score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) +{ + if (!proxy_.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); + + const ActivateRunTargetRequest request{ + runTargetName, + force ? ActivationMode::kForced : ActivationMode::kQueued + }; + + auto result = proxy_->activate_run_target(request); + if (!result.has_value()) + { + DBG_PROXY("activate_run_target: proxy method call failed"); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == RequestStatus::kRejected) + { + DBG_PROXY("activate_run_target: rejected by LM"); + return score::MakeUnexpected(response.rejection_reason); + } + + DBG_PROXY("activate_run_target: accepted by LM"); + return {}; +} + +score::Result LmControlImpl::register_run_target_activation_callback(ActivationCallback callback) +{ + if (!callback) + { + return score::MakeUnexpected(ExecErrc::kInvalidArguments); + } + std::lock_guard lock{callback_mutex_}; + callback_ = std::move(callback); + DBG_PROXY("Activation callback registered"); + return {}; +} + +score::Result LmControlImpl::get_active_run_target() +{ + if (!proxy_.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + DBG_PROXY("get_active_run_target: calling proxy"); + + auto result = proxy_->get_active_run_target(); + if (!result.has_value()) + { + DBG_PROXY("get_active_run_target: proxy method call failed"); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == QueryStatus::kNotAvailable) + { + DBG_PROXY("get_active_run_target: LM reports kActivationInProgress"); + return score::MakeUnexpected(response.rejection_reason); + } + + DBG_PROXY("get_active_run_target: returned '" << response.run_target << "'"); + return response.run_target; +} + +void LmControlImpl::OnActivationResult() +{ + auto get_result = proxy_->activation_result.GetNewSamples( + [this](score::mw::com::SamplePtr sample) noexcept { + DBG_PROXY("activation_result received: run_target='" << sample->activated_run_target << "'"); + ActivationCallback cb; + { + std::lock_guard lock{callback_mutex_}; + cb = callback_; + } + if (cb) + { + cb(sample->activation_source, sample->activated_run_target); + DBG_PROXY("activation_result: user callback invoked"); + } + else + { + DBG_PROXY("activation_result: no callback registered — sample dropped"); + } + }, + 1U); + + if (get_result.has_value() && get_result.value() > 0U) + { + DBG_PROXY("GetNewSamples: processed " << get_result.value() << " sample(s)"); + } +} + +} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp new file mode 100644 index 000000000..1c400fdc5 --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -0,0 +1,76 @@ +/******************************************************************************** + * 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 SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP +#define SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP + +#include +#include +#include +#include +#include +#include + +#include "score/mw/lifecycle/ilm_control.hpp" +#include "lm_control_service.h" + +namespace score::mw::lifecycle +{ + +/// @brief Real mw::com proxy-based implementation of ILmControl. +/// +/// Connects to the Launch Manager's LmControlSkeleton via mw::com SHM transport. +/// Construction attempts service discovery with a configurable timeout — if the +/// LM skeleton is not found within that time, IsConnected() returns false and +/// ILmControl::Create returns kCommunicationError. +class LmControlImpl final : public ILmControl +{ + public: + static constexpr auto kFindServiceTimeout = std::chrono::seconds(2); + static constexpr auto kFindServiceInterval = std::chrono::milliseconds(100); + + explicit LmControlImpl(std::string_view instance_specifier) noexcept; + ~LmControlImpl() noexcept override; + + /// @brief Returns true if the proxy connected successfully during construction. + bool IsConnected() const noexcept { return proxy_.has_value(); } + + score::Result activate_run_target(RunTargetName runTargetName, + bool force = false) override; + + score::Result register_run_target_activation_callback( + ActivationCallback callback) override; + + score::Result get_active_run_target() override; + + static constexpr auto kPollingInterval = std::chrono::milliseconds(50); + + private: + void OnActivationResult(); + void PollLoop(); + + std::optional proxy_; + ActivationCallback callback_; + std::mutex callback_mutex_; + + // TODO: SetReceiveHandler is the intended mechanism for event notifications. + // The skeleton→proxy notification socket is established lazily by the skeleton + // on the first Send call, causing the first notification to be dropped. + // Polling reads directly from SHM and is not affected by this. + // Remove poller once the notification channel initialisation is resolved. + std::atomic stop_polling_{false}; + std::thread poller_; +}; + +} // namespace score::mw::lifecycle + +#endif // SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_not_working.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_not_working.cpp new file mode 100644 index 000000000..d2387616d --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_not_working.cpp @@ -0,0 +1,220 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#include "lm_control_impl.hpp" + +#include +#include +#include + +// DEBUG helpers — remove once IPC delivery is confirmed stable. +#define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl + +namespace score::mw::lifecycle +{ + +LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept + : proxy_{} + , callback_{} + , callback_mutex_{} +{ + auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); + if (!specifier_result.has_value()) + { + DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); + return; + } + const auto specifier = std::move(specifier_result).value(); + + // Poll FindService until the LM skeleton is available or timeout expires. + const auto deadline = std::chrono::steady_clock::now() + kFindServiceTimeout; + while (std::chrono::steady_clock::now() < deadline) + { + auto find_result = LmControlProxy::FindService(specifier); + if (find_result.has_value() && !find_result.value().empty()) + { + auto create_result = LmControlProxy::Create(find_result.value().front()); + if (create_result.has_value()) + { + proxy_.emplace(std::move(create_result).value()); + DBG_PROXY("Proxy created successfully"); + break; + } + else + { + DBG_PROXY("Proxy::Create failed"); + } + } + std::this_thread::sleep_for(kFindServiceInterval); + } + + if (!proxy_.has_value()) + { + DBG_PROXY("FindService timed out — no skeleton found within " << kFindServiceTimeout.count() << "s"); + return; + } + + // Subscribe to activation_result events — max 1 sample buffered. + // DEBUG: verify subscription succeeded and log state — remove once event delivery is confirmed stable. + { + auto subscribe_result = proxy_->activation_result.Subscribe(1U); + if (!subscribe_result.has_value()) + { + DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); + proxy_.reset(); + return; + } + + const auto state = proxy_->activation_result.GetSubscriptionState(); + const char* state_str = + state == score::mw::com::SubscriptionState::kSubscribed ? "kSubscribed" : + state == score::mw::com::SubscriptionState::kNotSubscribed ? "kNotSubscribed" : + state == score::mw::com::SubscriptionState::kSubscriptionPending ? "kSubscriptionPending" : + "unknown"; + // Expected: kSubscribed. kSubscriptionPending means the skeleton has not + // yet acknowledged — samples may arrive late or not at all. + DBG_PROXY("activation_result subscription state after Subscribe: " << state_str); + } + + // TODO: SetReceiveHandler is the intended mechanism — replace poller with: + // proxy_->activation_result.SetReceiveHandler([this]() { OnActivationResult(); }); + // The skeleton→proxy notification socket is established lazily on the first + // Send, causing the first notification to be dropped. Polling reads SHM + // directly and is not affected by this timing issue. + poller_ = std::thread([this]() { PollLoop(); }); + DBG_PROXY("Polling thread started (interval: " << kPollingInterval.count() << "ms)"); +} + +LmControlImpl::~LmControlImpl() noexcept +{ + stop_polling_ = true; + if (poller_.joinable()) + { + poller_.join(); + } + if (proxy_.has_value()) + { + proxy_->activation_result.Unsubscribe(); + DBG_PROXY("Unsubscribed and destroyed proxy"); + } +} + +score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) +{ + if (!proxy_.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); + + const ActivateRunTargetRequest request{ + runTargetName, + force ? ActivationMode::kForced : ActivationMode::kQueued + }; + + auto result = proxy_->activate_run_target(request); + if (!result.has_value()) + { + DBG_PROXY("activate_run_target: proxy method call failed"); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == RequestStatus::kRejected) + { + DBG_PROXY("activate_run_target: rejected by LM"); + return score::MakeUnexpected(response.rejection_reason); + } + + DBG_PROXY("activate_run_target: accepted by LM"); + return {}; +} + +score::Result LmControlImpl::register_run_target_activation_callback(ActivationCallback callback) +{ + if (!callback) + { + return score::MakeUnexpected(ExecErrc::kInvalidArguments); + } + std::lock_guard lock{callback_mutex_}; + callback_ = std::move(callback); + DBG_PROXY("Activation callback registered"); + return {}; +} + +score::Result LmControlImpl::get_active_run_target() +{ + if (!proxy_.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + DBG_PROXY("get_active_run_target: calling proxy"); + + auto result = proxy_->get_active_run_target(); + if (!result.has_value()) + { + DBG_PROXY("get_active_run_target: proxy method call failed"); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == QueryStatus::kNotAvailable) + { + DBG_PROXY("get_active_run_target: LM reports kActivationInProgress"); + return score::MakeUnexpected(response.rejection_reason); + } + + DBG_PROXY("get_active_run_target: returned '" << response.run_target << "'"); + return response.run_target; +} + +void LmControlImpl::PollLoop() +{ + while (!stop_polling_) + { + OnActivationResult(); + std::this_thread::sleep_for(kPollingInterval); + } +} + +void LmControlImpl::OnActivationResult() +{ + auto get_result = proxy_->activation_result.GetNewSamples( + [this](score::mw::com::SamplePtr sample) noexcept { + DBG_PROXY("activation_result received: run_target='" << sample->activated_run_target << "'"); + ActivationCallback cb; + { + std::lock_guard lock{callback_mutex_}; + cb = callback_; + } + if (cb) + { + cb(sample->activation_source, sample->activated_run_target); + DBG_PROXY("activation_result: user callback invoked"); + } + else + { + DBG_PROXY("activation_result: no callback registered — sample dropped"); + } + }, + 1U); + + if (get_result.has_value() && get_result.value() > 0U) + { + DBG_PROXY("GetNewSamples: processed " << get_result.value() << " sample(s)"); + } +} + +} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_not_working.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_not_working.cpp new file mode 100644 index 000000000..c91b17cc8 --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_not_working.cpp @@ -0,0 +1,210 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#include "lm_control_impl.hpp" + +#include +#include + +// DEBUG helpers — remove once IPC delivery is confirmed stable. +#define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl + +namespace score::mw::lifecycle +{ + +LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept + : proxy_{} + , callback_{} + , callback_mutex_{} +{ + auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); + if (!specifier_result.has_value()) + { + DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); + return; + } + const auto specifier = std::move(specifier_result).value(); + + // Poll FindService until the LM skeleton is available or timeout expires. + const auto deadline = std::chrono::steady_clock::now() + kFindServiceTimeout; + while (std::chrono::steady_clock::now() < deadline) + { + auto find_result = LmControlProxy::FindService(specifier); + if (find_result.has_value() && !find_result.value().empty()) + { + auto create_result = LmControlProxy::Create(find_result.value().front()); + if (create_result.has_value()) + { + proxy_.emplace(std::move(create_result).value()); + DBG_PROXY("Proxy created successfully"); + break; + } + else + { + DBG_PROXY("Proxy::Create failed"); + } + } + std::this_thread::sleep_for(kFindServiceInterval); + } + + if (!proxy_.has_value()) + { + DBG_PROXY("FindService timed out — no skeleton found within " << kFindServiceTimeout.count() << "s"); + return; + } + + // Subscribe to activation_result events — max 1 sample buffered. + // DEBUG: verify subscription succeeded and log state — remove once event delivery is confirmed stable. + { + auto subscribe_result = proxy_->activation_result.Subscribe(1U); + if (!subscribe_result.has_value()) + { + DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); + proxy_.reset(); + return; + } + + const auto state = proxy_->activation_result.GetSubscriptionState(); + const char* state_str = + state == score::mw::com::SubscriptionState::kSubscribed ? "kSubscribed" : + state == score::mw::com::SubscriptionState::kNotSubscribed ? "kNotSubscribed" : + state == score::mw::com::SubscriptionState::kSubscriptionPending ? "kSubscriptionPending" : + "unknown"; + // Expected: kSubscribed. kSubscriptionPending means the skeleton has not + // yet acknowledged — samples may arrive late or not at all. + DBG_PROXY("activation_result subscription state after Subscribe: " << state_str); + } + + proxy_->activation_result.SetReceiveHandler([this]() { + OnActivationResult(); + }); + DBG_PROXY("SetReceiveHandler registered"); + + // Warmup: call GetNewSamples once to signal to the skeleton that this proxy + // is actively reading. This triggers the skeleton to establish its reverse + // notification channel (LoLa skeleton→proxy socket) before the first event + // is sent, so the SetReceiveHandler notification arrives reliably. + proxy_->activation_result.GetNewSamples( + [](score::mw::com::SamplePtr) noexcept {}, 1U); + DBG_PROXY("Warmup GetNewSamples called"); +} + +LmControlImpl::~LmControlImpl() noexcept +{ + if (proxy_.has_value()) + { + proxy_->activation_result.Unsubscribe(); + DBG_PROXY("Unsubscribed and destroyed proxy"); + } +} + +score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) +{ + if (!proxy_.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); + + const ActivateRunTargetRequest request{ + runTargetName, + force ? ActivationMode::kForced : ActivationMode::kQueued + }; + + auto result = proxy_->activate_run_target(request); + if (!result.has_value()) + { + DBG_PROXY("activate_run_target: proxy method call failed"); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == RequestStatus::kRejected) + { + DBG_PROXY("activate_run_target: rejected by LM"); + return score::MakeUnexpected(response.rejection_reason); + } + + DBG_PROXY("activate_run_target: accepted by LM"); + return {}; +} + +score::Result LmControlImpl::register_run_target_activation_callback(ActivationCallback callback) +{ + if (!callback) + { + return score::MakeUnexpected(ExecErrc::kInvalidArguments); + } + std::lock_guard lock{callback_mutex_}; + callback_ = std::move(callback); + DBG_PROXY("Activation callback registered"); + return {}; +} + +score::Result LmControlImpl::get_active_run_target() +{ + if (!proxy_.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + DBG_PROXY("get_active_run_target: calling proxy"); + + auto result = proxy_->get_active_run_target(); + if (!result.has_value()) + { + DBG_PROXY("get_active_run_target: proxy method call failed"); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == QueryStatus::kNotAvailable) + { + DBG_PROXY("get_active_run_target: LM reports kActivationInProgress"); + return score::MakeUnexpected(response.rejection_reason); + } + + DBG_PROXY("get_active_run_target: returned '" << response.run_target << "'"); + return response.run_target; +} + +void LmControlImpl::OnActivationResult() +{ + auto get_result = proxy_->activation_result.GetNewSamples( + [this](score::mw::com::SamplePtr sample) noexcept { + DBG_PROXY("activation_result received: run_target='" << sample->activated_run_target << "'"); + ActivationCallback cb; + { + std::lock_guard lock{callback_mutex_}; + cb = callback_; + } + if (cb) + { + cb(sample->activation_source, sample->activated_run_target); + DBG_PROXY("activation_result: user callback invoked"); + } + else + { + DBG_PROXY("activation_result: no callback registered — sample dropped"); + } + }, + 1U); + + if (get_result.has_value() && get_result.value() > 0U) + { + DBG_PROXY("GetNewSamples: processed " << get_result.value() << " sample(s)"); + } +} + +} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_service.h b/score/launch_manager/src/lm_control/src/details/lm_control_service.h new file mode 100644 index 000000000..bdf658f0b --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_service.h @@ -0,0 +1,163 @@ +/******************************************************************************** + * 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 SCORE_MW_LIFECYCLE_LM_CONTROL_SERVICE_H_ +#define SCORE_MW_LIFECYCLE_LM_CONTROL_SERVICE_H_ + +#include + +#include "score/mw/com/types.h" +#include "score/mw/lifecycle/ilm_control.hpp" + +namespace score::mw::lifecycle +{ + +// ============================================================================ +// activate_run_target +// ============================================================================ + +/// @brief Controls how an activation request interacts with any in-progress activation. +/// +/// Mirrors the `force` parameter of `ILmControl::activate_run_target`. The +/// underlying storage is `uint8_t` to keep the wire representation compact. +enum class ActivationMode : uint8_t +{ + kQueued = 0, ///< Queue this request behind any in-progress activation. + kForced = 1, ///< Cancel any in-progress activation and the pending queue, + ///< then start this activation immediately. +}; + +/// @brief Request sent by the State Manager to activate a Run Target. +struct ActivateRunTargetRequest +{ + /// @brief Name of the Run Target to activate. + RunTargetName run_target_name; + + /// @brief Whether to queue or force this activation. + ActivationMode mode; +}; + +/// @brief Indicates whether the request was accepted or rejected by the Launch Manager. +enum class RequestStatus : uint8_t +{ + kAccepted = 0, ///< The request was accepted. + kRejected = 1, ///< The request was rejected; see the accompanying rejection_reason field. +}; + +/// @brief Synchronous response to an activate_run_target request. +/// +/// Carries only the immediate acceptance/rejection decision. Activation +/// completion is notified separately via the `activation_result` event. +struct ActivateRunTargetResponse +{ + /// @brief Whether the Launch Manager accepted or rejected the request. + RequestStatus status; + + /// @brief Reason for rejection. Only meaningful when status == RequestStatus::kRejected. + ExecErrc rejection_reason; +}; + +// ============================================================================ +// get_active_run_target +// ============================================================================ + +/// @brief Indicates whether the Launch Manager can provide the currently active Run Target. +enum class QueryStatus : uint8_t +{ + kAvailable = 0, ///< A settled Run Target exists; see the run_target field. + kNotAvailable = 1, ///< No settled Run Target; an activation is in progress. +}; + +/// @brief Synchronous response to a get_active_run_target query. +struct GetActiveRunTargetResponse +{ + /// @brief Whether a settled Run Target is currently available. + QueryStatus status; + + /// @brief The currently active Run Target. + /// Only meaningful when status == QueryStatus::kAvailable. + RunTargetName run_target; + + /// @brief Reason the active Run Target is not available. + /// Only meaningful when status == QueryStatus::kNotAvailable. + /// Expected value: kActivationInProgress. + ExecErrc rejection_reason; +}; + +// ============================================================================ +// activation_result event +// ============================================================================ + +/// @brief Event sent by the Launch Manager when a Run Target activation settles. +/// +/// Sent to all subscribers regardless of which State Manager originated the request. +/// Activation always resolves into some Run Target — it cannot fail at the graph +/// level. The settled Run Target may differ from the one originally requested if a +/// preempting request or recovery action redirected the transition. +struct ActivationResult +{ + /// @brief The Run Target the Launch Manager settled on. + RunTargetName activated_run_target; + + /// @brief What caused this activation to occur. + RunTargetActivationSource activation_source; +}; + +// ============================================================================ +// Service definition +// ============================================================================ + +/// @brief mw::com service definition for the Launch Manager control interface. +/// +/// The State Manager holds a `LmControlProxy` and calls `activate_run_target` +/// and `get_active_run_target`. The Launch Manager holds a `LmControlSkeleton`, +/// registers handlers for those methods, and fires `activation_result` events +/// on Run Target settling. +template +class LmControlService : public Trait::Base +{ + public: + using Trait::Base::Base; + + /// @brief Request Run Target activation. + /// + /// Returns an `ActivateRunTargetResponse` synchronously indicating whether + /// the request was accepted or rejected (e.g. queue full, unknown Run Target). + /// If accepted, the actual activation result arrives asynchronously via + /// the `activation_result` event. + typename Trait::template Method + activate_run_target{*this, "ActivateRunTarget"}; + + /// @brief Query the currently active Run Target. + /// + /// Returns a `GetActiveRunTargetResponse` synchronously. If an activation is + /// in progress, status is QueryStatus::kNotAvailable and rejection_reason + /// carries kActivationInProgress. The caller should wait for the + /// activation_result event and retry if needed. + typename Trait::template Method + get_active_run_target{*this, "GetActiveRunTarget"}; + + /// @brief Event fired by the Launch Manager when a Run Target activation settles. + /// + /// All connected proxies receive this event. Each subscriber's registered + /// ActivationCallback is invoked with the settled Run Target and source. + typename Trait::template Event + activation_result{*this, "ActivationResult"}; +}; + +using LmControlProxy = score::mw::com::AsProxy; +using LmControlSkeleton = score::mw::com::AsSkeleton; + +} // namespace score::mw::lifecycle + +#endif // SCORE_MW_LIFECYCLE_LM_CONTROL_SERVICE_H_ diff --git a/tests/integration/lm_control/BUILD b/tests/integration/lm_control/BUILD new file mode 100644 index 000000000..dc43892e6 --- /dev/null +++ b/tests/integration/lm_control/BUILD @@ -0,0 +1,51 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_test") +load("//tests/utils/bazel:integration.bzl", "integration_test") + +# --------------------------------------------------------------------------- +# IPC integration test (real mw::com SHM transport between two processes) +# --------------------------------------------------------------------------- + +cc_binary( + name = "lm_skeleton_stub", + srcs = ["lm_skeleton_stub.cpp"], + deps = [ + "//score/launch_manager/src/daemon/src/control:lm_control_server", + "@score_communication//score/mw/com", + ], +) + +cc_binary( + name = "lm_control_ipc_test", + srcs = ["lm_control_ipc_test.cpp"], + deps = [ + "//score/launch_manager/src/lm_control:lm_control", + "//tests/utils/test_helper", + "@googletest//:gtest", + "@score_communication//score/mw/com", + ], +) + +integration_test( + name = "lm_control_ipc", + srcs = ["lm_control_ipc_test.py"], + binaries = [ + ":lm_skeleton_stub", + ":lm_control_ipc_test", + ], + files = [ + "lm_mw_com_config.json", + "sm_mw_com_config.json", + ], +) diff --git a/tests/integration/lm_control/lm_control_ipc_test.cpp b/tests/integration/lm_control/lm_control_ipc_test.cpp new file mode 100644 index 000000000..880374f86 --- /dev/null +++ b/tests/integration/lm_control/lm_control_ipc_test.cpp @@ -0,0 +1,127 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +#include + +#include +#include +#include + +#include "score/mw/com/runtime.h" +#include "score/mw/com/runtime_configuration.h" +#include "score/mw/lifecycle/ilm_control.hpp" +#include "tests/utils/test_helper/test_helper.hpp" + +namespace score::mw::lifecycle +{ + +class LmControlIPCTest : public ::testing::Test +{ + protected: + void SetUp() override + { + auto result = ILmControl::Create("StateManager/LaunchManager/Instance"); + ASSERT_TRUE(result.has_value()) << "ILmControl::Create failed — is lm_skeleton_stub running?"; + lm_ = std::move(result).value(); + } + + std::unique_ptr lm_; +}; + +TEST_F(LmControlIPCTest, GetActiveRunTargetReturnsValue) +{ + auto result = lm_->get_active_run_target(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "Running"); +} + +TEST_F(LmControlIPCTest, ActivateRunTargetIsAccepted) +{ + std::promise name_promise; + auto future = name_promise.get_future(); + + lm_->register_run_target_activation_callback( + [&name_promise](RunTargetActivationSource /*source*/, RunTargetName name) { + name_promise.set_value(name); + }); + + auto result = lm_->activate_run_target("Running"); + ASSERT_TRUE(result.has_value()); + + ASSERT_EQ(future.wait_for(std::chrono::seconds(3)), std::future_status::ready); + EXPECT_EQ(future.get(), "Running"); +} + +TEST_F(LmControlIPCTest, ActivateRunTargetFiresCallback) +{ + std::promise name_promise; + auto future = name_promise.get_future(); + + auto reg = lm_->register_run_target_activation_callback( + [&name_promise](RunTargetActivationSource /*source*/, RunTargetName name) { + name_promise.set_value(name); + }); + ASSERT_TRUE(reg.has_value()); + + auto result = lm_->activate_run_target("ABC"); + //auto result = lm_->activate_run_target("Running"); + ASSERT_TRUE(result.has_value()); + + ASSERT_EQ(future.wait_for(std::chrono::seconds(3)), std::future_status::ready); + EXPECT_EQ(future.get(), "ABC"); + //EXPECT_EQ(future.get(), "Running"); +} + +TEST_F(LmControlIPCTest, CallbackReceivesStateManagerSource) +{ + std::promise source_promise; + std::promise name_promise; + auto source_future = source_promise.get_future(); + auto name_future = name_promise.get_future(); + + lm_->register_run_target_activation_callback( + [&source_promise, &name_promise](RunTargetActivationSource source, RunTargetName name) { + source_promise.set_value(source); + name_promise.set_value(name); + }); + + auto result = lm_->activate_run_target("Test_State"); + //auto result = lm_->activate_run_target("Running"); + ASSERT_TRUE(result.has_value()); + + ASSERT_EQ(source_future.wait_for(std::chrono::seconds(3)), std::future_status::ready); + EXPECT_EQ(source_future.get(), RunTargetActivationSource::kStateManagerRequest); + EXPECT_EQ(name_future.get(), "Test_State"); +} + +} // namespace score::mw::lifecycle + +/// Usage: lm_control_ipc_test +int main(int argc, const char* argv[]) +{ + if (argc < 2) + { + std::cerr << "Usage: lm_control_ipc_test \n"; + return 1; + } + + score::mw::com::runtime::InitializeRuntime( + score::mw::com::runtime::RuntimeConfiguration{argv[1]}); + + int ret; + { + ret = TestRunner(__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd).RunTests(); + } + + sleep(1); + return ret; +} diff --git a/tests/integration/lm_control/lm_control_ipc_test.py b/tests/integration/lm_control/lm_control_ipc_test.py new file mode 100644 index 000000000..47be34737 --- /dev/null +++ b/tests/integration/lm_control/lm_control_ipc_test.py @@ -0,0 +1,78 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +import logging +import time + +from attribute_plugin import add_test_properties +from tests.utils.testing_utils.run_until_file_deployed import run_until_file_deployed +from tests.utils.testing_utils.test_results import assert_test_results +from tests.utils.testing_utils.setup_test import setup_test + + +logger = logging.getLogger(__name__) + + +@add_test_properties( + partially_verifies=[], + test_type="interface-test", + derivation_technique="explorative-testing", +) +def test_lm_control_ipc(target, setup_test, assert_test_results, remote_test_dir): + """ + Objective: Verifies end-to-end IPC communication between the LmControlSkeleton + (Launch Manager side) and the ILmControl proxy (State Manager side) over mw::com SHM. + + The lm_skeleton_stub starts the LmControlServer with stub handlers. + The lm_control_ipc_test binary connects via ILmControl, exercises activate_run_target + and get_active_run_target, and verifies that callbacks fire correctly over IPC. + + Expected Behaviour: All ILmControl API calls succeed and the activation callback + is invoked with the correct RunTargetName and RunTargetActivationSource. + """ + test_dir = remote_test_dir + + lm_config = str(test_dir / "lm_mw_com_config.json") + sm_config = str(test_dir / "sm_mw_com_config.json") + lm_stub = str(test_dir / "lm_skeleton_stub") + sm_client = str(test_dir / "lm_control_ipc_test") + + # Start the LM skeleton stub — runs in the background. + logger.info("Starting lm_skeleton_stub") + lm_proc = target.execute_async(lm_stub, args=[lm_config], cwd=str(test_dir)) + + # Wait for the skeleton to offer the service. + # ILmControl::Create polls for up to 2 s, so 3 s gives ample margin. + time.sleep(3) + + if not lm_proc.is_running(): + raise RuntimeError( + f"lm_skeleton_stub exited early (code {lm_proc.get_exit_code()})" + ) + + # Run the SM client. It signals completion by creating the test_end file + # (TerminationNotification::kTestEnd), then waits in pause() for SIGTERM. + # run_until_file_deployed detects the file, sends SIGTERM, and the binary exits. + logger.info("Running lm_control_ipc_test") + run_until_file_deployed( + target=target, + binary_path=sm_client, + file_path=remote_test_dir.parent / "test_end", + cwd=str(remote_test_dir), + args=[sm_config], + timeout_s=30.0, + ) + + # Shut down the LM skeleton. + lm_proc.stop() + + assert_test_results({"lm_control_ipc_test.xml"}) diff --git a/tests/integration/lm_control/lm_mw_com_config.json b/tests/integration/lm_control/lm_mw_com_config.json new file mode 100644 index 000000000..9d84dd451 --- /dev/null +++ b/tests/integration/lm_control/lm_mw_com_config.json @@ -0,0 +1,76 @@ +{ + "serviceTypes": [ + { + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "bindings": [ + { + "binding": "SHM", + "serviceId": 7001, + "events": [ + { + "eventName": "ActivationResult", + "eventId": 1 + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "methodId": 2 + }, + { + "methodName": "GetActiveRunTarget", + "methodId": 3 + } + ] + } + ] + } + ], + "serviceInstances": [ + { + "instanceSpecifier": "LaunchManager/StateManager/Instance", + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "ActivationResult", + "numberOfSampleSlots": 1, + "maxSubscribers": 5 + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "queueSize": 1 + }, + { + "methodName": "GetActiveRunTarget", + "queueSize": 1 + } + ], + "allowedConsumer": { + "QM": [0] + }, + "allowedProvider": { + "QM": [0] + } + } + ] + } + ], + "global": { + "asil-level": "QM" + } +} diff --git a/tests/integration/lm_control/lm_skeleton_stub.cpp b/tests/integration/lm_control/lm_skeleton_stub.cpp new file mode 100644 index 000000000..490eee194 --- /dev/null +++ b/tests/integration/lm_control/lm_skeleton_stub.cpp @@ -0,0 +1,68 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +#include +#include +#include +#include +#include + +#include "score/mw/com/runtime.h" +#include "score/mw/com/runtime_configuration.h" +#include "score/mw/launch_manager/control/lm_control_server.hpp" + +namespace +{ +std::atomic g_stop{false}; + +void on_signal(int) noexcept +{ + g_stop = true; +} +} // namespace + +/// @brief Minimal binary that starts the LmControlSkeleton (stub handlers) and +/// waits for SIGTERM. Used as the Launch Manager side in IPC integration tests. +/// +/// Usage: lm_skeleton_stub +int main(int argc, const char* argv[]) +{ + if (argc < 2) + { + std::cerr << "Usage: lm_skeleton_stub \n"; + return 1; + } + + std::signal(SIGTERM, on_signal); + std::signal(SIGINT, on_signal); + + score::mw::com::runtime::InitializeRuntime( + score::mw::com::runtime::RuntimeConfiguration{argv[1]}); + + score::mw::launch_manager::control::LmControlServer server{}; + if (!server.Initialize()) + { + std::cerr << "lm_skeleton_stub: LmControlServer::Initialize failed\n"; + return 1; + } + + std::cout << "lm_skeleton_stub: skeleton ready\n"; + std::cout.flush(); + + while (!g_stop) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + server.Shutdown(); + return 0; +} diff --git a/tests/integration/lm_control/sm_mw_com_config.json b/tests/integration/lm_control/sm_mw_com_config.json new file mode 100644 index 000000000..1748fdbf8 --- /dev/null +++ b/tests/integration/lm_control/sm_mw_com_config.json @@ -0,0 +1,74 @@ +{ + "serviceTypes": [ + { + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "bindings": [ + { + "binding": "SHM", + "serviceId": 7001, + "events": [ + { + "eventName": "ActivationResult", + "eventId": 1 + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "methodId": 2 + }, + { + "methodName": "GetActiveRunTarget", + "methodId": 3 + } + ] + } + ] + } + ], + "serviceInstances": [ + { + "instanceSpecifier": "StateManager/LaunchManager/Instance", + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "ActivationResult" + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "queueSize": 1 + }, + { + "methodName": "GetActiveRunTarget", + "queueSize": 1 + } + ], + "allowedConsumer": { + "QM": [0] + }, + "allowedProvider": { + "QM": [0] + } + } + ] + } + ], + "global": { + "asil-level": "QM" + } +} From 22d6798fc247aa762b59f470f58f66a3d808a25f Mon Sep 17 00:00:00 2001 From: SimonKozik <244535158+SimonKozik@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:34:06 +0100 Subject: [PATCH 02/27] Removing unneeded call to GetNewSamples --- .../src/lm_control/src/details/lm_control_impl.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp index c91b17cc8..168cc8181 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp @@ -90,13 +90,19 @@ LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept }); DBG_PROXY("SetReceiveHandler registered"); + // Looks like the Warmup call is not needed... + // Maybe the problem was sending the event from the + // LmControlServer::OnActivateRunTarget() --> skeleton side + // Maybe we need to send events from a separate thread... + // But why polling is not working??? + // // Warmup: call GetNewSamples once to signal to the skeleton that this proxy // is actively reading. This triggers the skeleton to establish its reverse // notification channel (LoLa skeleton→proxy socket) before the first event // is sent, so the SetReceiveHandler notification arrives reliably. - proxy_->activation_result.GetNewSamples( - [](score::mw::com::SamplePtr) noexcept {}, 1U); - DBG_PROXY("Warmup GetNewSamples called"); + //proxy_->activation_result.GetNewSamples( + // [](score::mw::com::SamplePtr) noexcept {}, 1U); + //DBG_PROXY("Warmup GetNewSamples called"); } LmControlImpl::~LmControlImpl() noexcept From 964357b466fc463eed7b01c1d425729f33f0483c Mon Sep 17 00:00:00 2001 From: SimonKozik <244535158+SimonKozik@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:26:37 +0100 Subject: [PATCH 03/27] Saving fixed versions for polling and SetReceiveHandler --- .../lm_control_impl_PollLoop_working.cpp | 236 ++++++++++++++++++ ...ontrol_impl__SetReceiveHandler_working.cpp | 216 ++++++++++++++++ 2 files changed, 452 insertions(+) create mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_working.cpp create mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_working.cpp diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_working.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_working.cpp new file mode 100644 index 000000000..5362aee8d --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_working.cpp @@ -0,0 +1,236 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#include "lm_control_impl.hpp" + +#include +#include +#include + +// DEBUG helpers — remove once IPC delivery is confirmed stable. +#define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl + +namespace score::mw::lifecycle +{ + +LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept + : proxy_{} + , callback_{} + , callback_mutex_{} +{ + auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); + if (!specifier_result.has_value()) + { + DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); + return; + } + const auto specifier = std::move(specifier_result).value(); + + // Poll FindService until the LM skeleton is available or timeout expires. + const auto deadline = std::chrono::steady_clock::now() + kFindServiceTimeout; + while (std::chrono::steady_clock::now() < deadline) + { + auto find_result = LmControlProxy::FindService(specifier); + if (find_result.has_value() && !find_result.value().empty()) + { + auto create_result = LmControlProxy::Create(find_result.value().front()); + if (create_result.has_value()) + { + proxy_.emplace(std::move(create_result).value()); + DBG_PROXY("Proxy created successfully"); + break; + } + else + { + DBG_PROXY("Proxy::Create failed"); + } + } + std::this_thread::sleep_for(kFindServiceInterval); + } + + if (!proxy_.has_value()) + { + DBG_PROXY("FindService timed out — no skeleton found within " << kFindServiceTimeout.count() << "s"); + return; + } + + // Subscribe to activation_result events — max 1 sample buffered. + // DEBUG: verify subscription succeeded and log state — remove once event delivery is confirmed stable. + { + auto subscribe_result = proxy_->activation_result.Subscribe(1U); + if (!subscribe_result.has_value()) + { + DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); + proxy_.reset(); + return; + } + + const auto state = proxy_->activation_result.GetSubscriptionState(); + const char* state_str = + state == score::mw::com::SubscriptionState::kSubscribed ? "kSubscribed" : + state == score::mw::com::SubscriptionState::kNotSubscribed ? "kNotSubscribed" : + state == score::mw::com::SubscriptionState::kSubscriptionPending ? "kSubscriptionPending" : + "unknown"; + // Expected: kSubscribed. kSubscriptionPending means the skeleton has not + // yet acknowledged — samples may arrive late or not at all. + DBG_PROXY("activation_result subscription state after Subscribe: " << state_str); + } + + // Drain any stale event left in the SHM slot from a previous subscription. + // With numberOfSampleSlots=1 and no enforceMaxSamples, a delayed Send from + // a previous subscriber's activation can still be in the slot when a new + // subscriber starts. Consuming it here prevents the new subscriber's callback + // from firing with a stale result. + // + // Please also note that consuming an event (the GetNewSamples call) doesn't clear + // the data from the SHM slot. It only advances the read pointer for that specific proxy. + // When somebody creates a brand new proxy and calls Subscribe, it gets a fresh read + // pointer starting from the beginning of the circular buffer. From the new + // proxy's perspective, the Running data in the SHM slot looks like a new, unread event. + // So it will create false callback if the buffer is undrained. + proxy_->activation_result.GetNewSamples( + [](score::mw::com::SamplePtr) noexcept {}, 1U); + DBG_PROXY("Stale event drain done"); + + // TODO: SetReceiveHandler is the intended mechanism — replace poller with: + // proxy_->activation_result.SetReceiveHandler([this]() { OnActivationResult(); }); + // The skeleton→proxy notification socket is established lazily on the first + // Send, causing the first notification to be dropped. Polling reads SHM + // directly and is not affected by this timing issue. + poller_ = std::thread([this]() { PollLoop(); }); + DBG_PROXY("Polling thread started (interval: " << kPollingInterval.count() << "ms)"); +} + +LmControlImpl::~LmControlImpl() noexcept +{ + stop_polling_ = true; + if (poller_.joinable()) + { + poller_.join(); + } + if (proxy_.has_value()) + { + proxy_->activation_result.Unsubscribe(); + DBG_PROXY("Unsubscribed and destroyed proxy"); + } +} + +score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) +{ + if (!proxy_.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); + + const ActivateRunTargetRequest request{ + runTargetName, + force ? ActivationMode::kForced : ActivationMode::kQueued + }; + + auto result = proxy_->activate_run_target(request); + if (!result.has_value()) + { + DBG_PROXY("activate_run_target: proxy method call failed"); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == RequestStatus::kRejected) + { + DBG_PROXY("activate_run_target: rejected by LM"); + return score::MakeUnexpected(response.rejection_reason); + } + + DBG_PROXY("activate_run_target: accepted by LM"); + return {}; +} + +score::Result LmControlImpl::register_run_target_activation_callback(ActivationCallback callback) +{ + if (!callback) + { + return score::MakeUnexpected(ExecErrc::kInvalidArguments); + } + std::lock_guard lock{callback_mutex_}; + callback_ = std::move(callback); + DBG_PROXY("Activation callback registered"); + return {}; +} + +score::Result LmControlImpl::get_active_run_target() +{ + if (!proxy_.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + DBG_PROXY("get_active_run_target: calling proxy"); + + auto result = proxy_->get_active_run_target(); + if (!result.has_value()) + { + DBG_PROXY("get_active_run_target: proxy method call failed"); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == QueryStatus::kNotAvailable) + { + DBG_PROXY("get_active_run_target: LM reports kActivationInProgress"); + return score::MakeUnexpected(response.rejection_reason); + } + + DBG_PROXY("get_active_run_target: returned '" << response.run_target << "'"); + return response.run_target; +} + +void LmControlImpl::PollLoop() +{ + while (!stop_polling_) + { + OnActivationResult(); + std::this_thread::sleep_for(kPollingInterval); + } +} + +void LmControlImpl::OnActivationResult() +{ + auto get_result = proxy_->activation_result.GetNewSamples( + [this](score::mw::com::SamplePtr sample) noexcept { + DBG_PROXY("activation_result received: run_target='" << sample->activated_run_target << "'"); + ActivationCallback cb; + { + std::lock_guard lock{callback_mutex_}; + cb = callback_; + } + if (cb) + { + cb(sample->activation_source, sample->activated_run_target); + DBG_PROXY("activation_result: user callback invoked"); + } + else + { + DBG_PROXY("activation_result: no callback registered — sample dropped"); + } + }, + 1U); + + if (get_result.has_value() && get_result.value() > 0U) + { + DBG_PROXY("GetNewSamples: processed " << get_result.value() << " sample(s)"); + } +} + +} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_working.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_working.cpp new file mode 100644 index 000000000..168cc8181 --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_working.cpp @@ -0,0 +1,216 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#include "lm_control_impl.hpp" + +#include +#include + +// DEBUG helpers — remove once IPC delivery is confirmed stable. +#define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl + +namespace score::mw::lifecycle +{ + +LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept + : proxy_{} + , callback_{} + , callback_mutex_{} +{ + auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); + if (!specifier_result.has_value()) + { + DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); + return; + } + const auto specifier = std::move(specifier_result).value(); + + // Poll FindService until the LM skeleton is available or timeout expires. + const auto deadline = std::chrono::steady_clock::now() + kFindServiceTimeout; + while (std::chrono::steady_clock::now() < deadline) + { + auto find_result = LmControlProxy::FindService(specifier); + if (find_result.has_value() && !find_result.value().empty()) + { + auto create_result = LmControlProxy::Create(find_result.value().front()); + if (create_result.has_value()) + { + proxy_.emplace(std::move(create_result).value()); + DBG_PROXY("Proxy created successfully"); + break; + } + else + { + DBG_PROXY("Proxy::Create failed"); + } + } + std::this_thread::sleep_for(kFindServiceInterval); + } + + if (!proxy_.has_value()) + { + DBG_PROXY("FindService timed out — no skeleton found within " << kFindServiceTimeout.count() << "s"); + return; + } + + // Subscribe to activation_result events — max 1 sample buffered. + // DEBUG: verify subscription succeeded and log state — remove once event delivery is confirmed stable. + { + auto subscribe_result = proxy_->activation_result.Subscribe(1U); + if (!subscribe_result.has_value()) + { + DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); + proxy_.reset(); + return; + } + + const auto state = proxy_->activation_result.GetSubscriptionState(); + const char* state_str = + state == score::mw::com::SubscriptionState::kSubscribed ? "kSubscribed" : + state == score::mw::com::SubscriptionState::kNotSubscribed ? "kNotSubscribed" : + state == score::mw::com::SubscriptionState::kSubscriptionPending ? "kSubscriptionPending" : + "unknown"; + // Expected: kSubscribed. kSubscriptionPending means the skeleton has not + // yet acknowledged — samples may arrive late or not at all. + DBG_PROXY("activation_result subscription state after Subscribe: " << state_str); + } + + proxy_->activation_result.SetReceiveHandler([this]() { + OnActivationResult(); + }); + DBG_PROXY("SetReceiveHandler registered"); + + // Looks like the Warmup call is not needed... + // Maybe the problem was sending the event from the + // LmControlServer::OnActivateRunTarget() --> skeleton side + // Maybe we need to send events from a separate thread... + // But why polling is not working??? + // + // Warmup: call GetNewSamples once to signal to the skeleton that this proxy + // is actively reading. This triggers the skeleton to establish its reverse + // notification channel (LoLa skeleton→proxy socket) before the first event + // is sent, so the SetReceiveHandler notification arrives reliably. + //proxy_->activation_result.GetNewSamples( + // [](score::mw::com::SamplePtr) noexcept {}, 1U); + //DBG_PROXY("Warmup GetNewSamples called"); +} + +LmControlImpl::~LmControlImpl() noexcept +{ + if (proxy_.has_value()) + { + proxy_->activation_result.Unsubscribe(); + DBG_PROXY("Unsubscribed and destroyed proxy"); + } +} + +score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) +{ + if (!proxy_.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); + + const ActivateRunTargetRequest request{ + runTargetName, + force ? ActivationMode::kForced : ActivationMode::kQueued + }; + + auto result = proxy_->activate_run_target(request); + if (!result.has_value()) + { + DBG_PROXY("activate_run_target: proxy method call failed"); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == RequestStatus::kRejected) + { + DBG_PROXY("activate_run_target: rejected by LM"); + return score::MakeUnexpected(response.rejection_reason); + } + + DBG_PROXY("activate_run_target: accepted by LM"); + return {}; +} + +score::Result LmControlImpl::register_run_target_activation_callback(ActivationCallback callback) +{ + if (!callback) + { + return score::MakeUnexpected(ExecErrc::kInvalidArguments); + } + std::lock_guard lock{callback_mutex_}; + callback_ = std::move(callback); + DBG_PROXY("Activation callback registered"); + return {}; +} + +score::Result LmControlImpl::get_active_run_target() +{ + if (!proxy_.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + DBG_PROXY("get_active_run_target: calling proxy"); + + auto result = proxy_->get_active_run_target(); + if (!result.has_value()) + { + DBG_PROXY("get_active_run_target: proxy method call failed"); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == QueryStatus::kNotAvailable) + { + DBG_PROXY("get_active_run_target: LM reports kActivationInProgress"); + return score::MakeUnexpected(response.rejection_reason); + } + + DBG_PROXY("get_active_run_target: returned '" << response.run_target << "'"); + return response.run_target; +} + +void LmControlImpl::OnActivationResult() +{ + auto get_result = proxy_->activation_result.GetNewSamples( + [this](score::mw::com::SamplePtr sample) noexcept { + DBG_PROXY("activation_result received: run_target='" << sample->activated_run_target << "'"); + ActivationCallback cb; + { + std::lock_guard lock{callback_mutex_}; + cb = callback_; + } + if (cb) + { + cb(sample->activation_source, sample->activated_run_target); + DBG_PROXY("activation_result: user callback invoked"); + } + else + { + DBG_PROXY("activation_result: no callback registered — sample dropped"); + } + }, + 1U); + + if (get_result.has_value() && get_result.value() > 0U) + { + DBG_PROXY("GetNewSamples: processed " << get_result.value() << " sample(s)"); + } +} + +} // namespace score::mw::lifecycle From bfe62c75e03b5f240f4fdd256a509ac84518ff92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 14 Aug 2026 14:06:11 +0200 Subject: [PATCH 04/27] Fix MODULE.bazel dependency --- MODULE.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MODULE.bazel b/MODULE.bazel index cf81bf64a..d14d41902 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -185,7 +185,7 @@ bazel_dep(name = "score_communication") git_override( module_name = "score_communication", commit = "bcafda8fff9a90bc9e32bf054ad94fb7a627b318", - remote = "git@github.com:eclipse-score/communication.git", + remote = "https://github.com/eclipse-score/communication.git", ) # Hedron's Compile Commands Extractor for Bazel From 5a94417e1d564e8f6827343fe2aa2799feecfb5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 14 Aug 2026 14:06:28 +0200 Subject: [PATCH 05/27] Fix test case --- .../lm_control/lm_control_ipc_test.cpp | 115 ++++++++---------- 1 file changed, 51 insertions(+), 64 deletions(-) diff --git a/tests/integration/lm_control/lm_control_ipc_test.cpp b/tests/integration/lm_control/lm_control_ipc_test.cpp index 880374f86..ab24c8db7 100644 --- a/tests/integration/lm_control/lm_control_ipc_test.cpp +++ b/tests/integration/lm_control/lm_control_ipc_test.cpp @@ -30,77 +30,65 @@ class LmControlIPCTest : public ::testing::Test void SetUp() override { auto result = ILmControl::Create("StateManager/LaunchManager/Instance"); - ASSERT_TRUE(result.has_value()) << "ILmControl::Create failed — is lm_skeleton_stub running?"; + ASSERT_TRUE(result.has_value()) << "ILmControl::Create failed"; lm_ = std::move(result).value(); } std::unique_ptr lm_; }; -TEST_F(LmControlIPCTest, GetActiveRunTargetReturnsValue) +TEST_F(LmControlIPCTest, CallingAPI) { - auto result = lm_->get_active_run_target(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result.value(), "Running"); -} - -TEST_F(LmControlIPCTest, ActivateRunTargetIsAccepted) -{ - std::promise name_promise; - auto future = name_promise.get_future(); - - lm_->register_run_target_activation_callback( - [&name_promise](RunTargetActivationSource /*source*/, RunTargetName name) { - name_promise.set_value(name); - }); - - auto result = lm_->activate_run_target("Running"); - ASSERT_TRUE(result.has_value()); - - ASSERT_EQ(future.wait_for(std::chrono::seconds(3)), std::future_status::ready); - EXPECT_EQ(future.get(), "Running"); -} + TEST_STEP("GetActiveRunTarget returns Running") + { + auto result = lm_->get_active_run_target(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "Running"); + } -TEST_F(LmControlIPCTest, ActivateRunTargetFiresCallback) -{ - std::promise name_promise; - auto future = name_promise.get_future(); - - auto reg = lm_->register_run_target_activation_callback( - [&name_promise](RunTargetActivationSource /*source*/, RunTargetName name) { - name_promise.set_value(name); - }); - ASSERT_TRUE(reg.has_value()); - - auto result = lm_->activate_run_target("ABC"); - //auto result = lm_->activate_run_target("Running"); - ASSERT_TRUE(result.has_value()); - - ASSERT_EQ(future.wait_for(std::chrono::seconds(3)), std::future_status::ready); - EXPECT_EQ(future.get(), "ABC"); - //EXPECT_EQ(future.get(), "Running"); -} + TEST_STEP("ActivateRunTarget is successful and triggers ActivationCallback with StateManagerRequest") + { + std::optional name{}; + std::optional source{}; + std::promise sync_promise; + auto future = sync_promise.get_future(); + + lm_->register_run_target_activation_callback( + [&sync_promise, &name, &source](RunTargetActivationSource s, RunTargetName n) { + source = s; + name = n; + sync_promise.set_value(); + }); + + auto result = lm_->activate_run_target("Running"); + ASSERT_TRUE(result.has_value()); + + ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); + EXPECT_EQ(name.value(), "Running"); + EXPECT_EQ(source.value(), RunTargetActivationSource::kStateManagerRequest); + } -TEST_F(LmControlIPCTest, CallbackReceivesStateManagerSource) -{ - std::promise source_promise; - std::promise name_promise; - auto source_future = source_promise.get_future(); - auto name_future = name_promise.get_future(); - - lm_->register_run_target_activation_callback( - [&source_promise, &name_promise](RunTargetActivationSource source, RunTargetName name) { - source_promise.set_value(source); - name_promise.set_value(name); - }); - - auto result = lm_->activate_run_target("Test_State"); - //auto result = lm_->activate_run_target("Running"); - ASSERT_TRUE(result.has_value()); - - ASSERT_EQ(source_future.wait_for(std::chrono::seconds(3)), std::future_status::ready); - EXPECT_EQ(source_future.get(), RunTargetActivationSource::kStateManagerRequest); - EXPECT_EQ(name_future.get(), "Test_State"); + TEST_STEP("ActivateRunTarget fails and activates recovery action. Triggers ActivationCallback with kRecoveryAction") + { + std::optional name{}; + std::optional source{}; + std::promise sync_promise; + auto future = sync_promise.get_future(); + + lm_->register_run_target_activation_callback( + [&sync_promise, &name, &source](RunTargetActivationSource s, RunTargetName n) { + source = s; + name = n; + sync_promise.set_value(); + }); + + auto result = lm_->activate_run_target("FailRunning"); + ASSERT_TRUE(result.has_value()); + + ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); + EXPECT_EQ(name.value(), "Recovery"); + EXPECT_EQ(source.value(), RunTargetActivationSource::kRecoveryAction); + } } } // namespace score::mw::lifecycle @@ -114,8 +102,7 @@ int main(int argc, const char* argv[]) return 1; } - score::mw::com::runtime::InitializeRuntime( - score::mw::com::runtime::RuntimeConfiguration{argv[1]}); + score::mw::com::runtime::InitializeRuntime(score::mw::com::runtime::RuntimeConfiguration{argv[1]}); int ret; { From 6ba5340d1a9830d75741be8ccbc950b73e568415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 14 Aug 2026 14:29:55 +0200 Subject: [PATCH 06/27] Refactor control client test --- .../daemon/src/control/lm_control_server.cpp | 92 ++++++++++--------- .../daemon/src/control/lm_control_server.hpp | 57 ++++++++++-- .../lm_control/lm_control_ipc_test.py | 2 +- .../lm_control/lm_skeleton_stub.cpp | 79 +++++++++++++++- 4 files changed, 171 insertions(+), 59 deletions(-) diff --git a/score/launch_manager/src/daemon/src/control/lm_control_server.cpp b/score/launch_manager/src/daemon/src/control/lm_control_server.cpp index 33ea5e81d..ef060402f 100644 --- a/score/launch_manager/src/daemon/src/control/lm_control_server.cpp +++ b/score/launch_manager/src/daemon/src/control/lm_control_server.cpp @@ -13,16 +13,10 @@ #include "lm_control_server.hpp" -#include #include -#include -#include #include "score/mw/launch_manager/common/log.hpp" -// DEBUG helpers — remove once IPC delivery is confirmed stable. -#define DBG_SKELETON(msg) std::cout<< "[LmControlServer DEBUG] " << msg << std::endl - namespace score::mw::launch_manager::control { @@ -32,8 +26,9 @@ constexpr std::string_view kDefaultInstanceSpecifier = "LaunchManager/StateManag constexpr const char* kInstanceSpecifierEnvVar = "SCORE_LCM_SKELETON_INSTANCE_SPECIFIER"; } // namespace -LmControlServer::LmControlServer(std::string_view instance_specifier) - : instance_specifier_{instance_specifier.empty() +LmControlServer::LmControlServer(IGraph& graph, std::string_view instance_specifier) + : graph_{graph} + , instance_specifier_{instance_specifier.empty() ? (std::getenv(kInstanceSpecifierEnvVar) != nullptr ? std::getenv(kInstanceSpecifierEnvVar) : std::string{kDefaultInstanceSpecifier}) @@ -114,41 +109,20 @@ void LmControlServer::Shutdown() lifecycle::ActivateRunTargetResponse LmControlServer::OnActivateRunTarget( const lifecycle::ActivateRunTargetRequest& request) { - LM_LOG_INFO() << "LmControlServer: ActivateRunTarget stub — run_target=" - << request.run_target_name; - DBG_SKELETON("OnActivateRunTarget: run_target='" << request.run_target_name << "'"); + LM_LOG_INFO() << "LmControlServer: ActivateRunTarget run_target=" << request.run_target_name; - // Send activation_result from a separate thread after a 1-second delay. - if (skeleton_.has_value()) - { - const auto run_target = request.run_target_name; - std::thread([this, run_target]() { - std::this_thread::sleep_for(std::chrono::seconds(1)); - - if (!skeleton_.has_value()) - { - DBG_SKELETON("activation_result: skeleton gone before delayed Send"); - return; - } - - auto alloc = skeleton_->activation_result.Allocate(); - if (!alloc.has_value()) - { - DBG_SKELETON("activation_result.Allocate() failed — event not sent"); - return; - } - - alloc.value().Get()->activated_run_target = run_target; - alloc.value().Get()->activation_source = - lifecycle::RunTargetActivationSource::kStateManagerRequest; - DBG_SKELETON("activation_result.Send: run_target='" << run_target << "'"); - skeleton_->activation_result.Send(std::move(alloc).value()); - DBG_SKELETON("activation_result.Send: done"); - }).detach(); - } - else + const bool force = (request.mode == lifecycle::ActivationMode::kForced); + const auto request_id = next_request_id_++; + const bool accepted = graph_.enqueueRunTargetActivation( + RunTargetRequest{request.run_target_name, force, request_id}); + + if (!accepted) { - DBG_SKELETON("OnActivateRunTarget: skeleton not available — event not sent"); + LM_LOG_ERROR() << "LmControlServer: ActivateRunTarget rejected run_target=" + << request.run_target_name; + return lifecycle::ActivateRunTargetResponse{ + lifecycle::RequestStatus::kRejected, + lifecycle::ExecErrc::kGeneralError}; } return lifecycle::ActivateRunTargetResponse{ @@ -159,14 +133,44 @@ lifecycle::ActivateRunTargetResponse LmControlServer::OnActivateRunTarget( lifecycle::GetActiveRunTargetResponse LmControlServer::OnGetActiveRunTarget() { - LM_LOG_DEBUG() << "LmControlServer: GetActiveRunTarget stub"; - DBG_SKELETON("OnGetActiveRunTarget: returning 'Running'"); + const auto active = graph_.getActiveRunTarget(); + if (!active.has_value()) + { + LM_LOG_DEBUG() << "LmControlServer: GetActiveRunTarget — activation in progress"; + return lifecycle::GetActiveRunTargetResponse{ + lifecycle::QueryStatus::kNotAvailable, + lifecycle::RunTargetName{}, + lifecycle::ExecErrc::kActivationInProgress}; + } + LM_LOG_DEBUG() << "LmControlServer: GetActiveRunTarget — returning " << active.value(); return lifecycle::GetActiveRunTargetResponse{ lifecycle::QueryStatus::kAvailable, - lifecycle::RunTargetName{"Running"}, + lifecycle::RunTargetName{active.value()}, lifecycle::ExecErrc::kGeneralError // unused when kAvailable }; } +void LmControlServer::activationCompleted(std::string_view runTarget, std::optional requestId) +{ + if (!skeleton_.has_value()) + { + LM_LOG_ERROR() << "LmControlServer: activationCompleted — skeleton not available, event not sent"; + return; + } + + auto alloc = skeleton_->activation_result.Allocate(); + if (!alloc.has_value()) + { + LM_LOG_ERROR() << "LmControlServer: activationCompleted — Allocate() failed, event not sent"; + return; + } + + alloc.value().Get()->activated_run_target = lifecycle::RunTargetName{runTarget}; + alloc.value().Get()->activation_source = + requestId.has_value() ? lifecycle::RunTargetActivationSource::kStateManagerRequest + : lifecycle::RunTargetActivationSource::kRecoveryAction; + skeleton_->activation_result.Send(std::move(alloc).value()); +} + } // namespace score::mw::launch_manager::control diff --git a/score/launch_manager/src/daemon/src/control/lm_control_server.hpp b/score/launch_manager/src/daemon/src/control/lm_control_server.hpp index 0b8e825d3..d0d52c6f8 100644 --- a/score/launch_manager/src/daemon/src/control/lm_control_server.hpp +++ b/score/launch_manager/src/daemon/src/control/lm_control_server.hpp @@ -13,6 +13,7 @@ #ifndef SCORE_MW_LAUNCH_MANAGER_CONTROL_LM_CONTROL_SERVER_HPP #define SCORE_MW_LAUNCH_MANAGER_CONTROL_LM_CONTROL_SERVER_HPP +#include #include #include @@ -21,22 +22,50 @@ namespace score::mw::launch_manager::control { +/// @brief Internal request to activate a Run Target, correlated to its +/// completion via requestId. +struct RunTargetRequest +{ + lifecycle::RunTargetName runTargetName; + bool force; + std::int32_t requestId; +}; + +/// @brief Abstraction over the process graph that LmControlServer delegates to. +class IGraph +{ + public: + virtual ~IGraph() noexcept = default; + + /// @return the currently active Run Target, or nullopt while an + /// activation is in progress. + virtual std::optional getActiveRunTarget() = 0; + + /// @return true if the request was accepted/enqueued, false if rejected. + virtual bool enqueueRunTargetActivation(RunTargetRequest request) = 0; + + protected: + IGraph() = default; + IGraph(const IGraph&) = default; + IGraph& operator=(const IGraph&) = default; + IGraph(IGraph&&) = default; + IGraph& operator=(IGraph&&) = default; +}; + /// @brief Hosts the mw::com LmControl skeleton on the Launch Manager side. /// /// Offers the LmControlService over mw::com so that State Managers can call -/// activate_run_target and get_active_run_target via the proxy. -/// -/// @note [transitional] Handlers are stubs — they do not delegate to -/// ProcessGroupManager. The activation result is sent immediately with -/// the requested Run Target echoed back. Replace with real -/// IGraphControl wiring in Stage 3. +/// activate_run_target and get_active_run_target via the proxy. Delegates +/// the actual Run Target querying and activation to an injected IGraph. class LmControlServer final { public: + /// @param graph The graph to delegate Run Target queries and + /// activations to. Must outlive this instance. /// @param instance_specifier The mw::com instance specifier for the /// skeleton. Reads SCORE_LCM_SKELETON_INSTANCE_SPECIFIER /// from the environment if empty. - explicit LmControlServer(std::string_view instance_specifier = {}); + explicit LmControlServer(IGraph& graph, std::string_view instance_specifier = {}); ~LmControlServer() noexcept; @@ -52,14 +81,24 @@ class LmControlServer final /// @brief Stop offering the service and destroy the skeleton. void Shutdown(); + /// @brief Notify that a Run Target activation has settled. Sends the + /// activation_result event to all subscribed proxies. + /// @param runTarget The Run Target the graph settled on. + /// @param requestId The id of the request that triggered this activation, + /// or nullopt if the activation was not tied to a + /// specific request (e.g. a recovery action). + void activationCompleted(std::string_view runTarget, std::optional requestId); + private: lifecycle::ActivateRunTargetResponse OnActivateRunTarget( const lifecycle::ActivateRunTargetRequest& request); lifecycle::GetActiveRunTargetResponse OnGetActiveRunTarget(); - std::string instance_specifier_; - std::optional skeleton_; + IGraph& graph_; + std::string instance_specifier_; + std::optional skeleton_; + std::int32_t next_request_id_{0}; }; } // namespace score::mw::launch_manager::control diff --git a/tests/integration/lm_control/lm_control_ipc_test.py b/tests/integration/lm_control/lm_control_ipc_test.py index 47be34737..118921fa4 100644 --- a/tests/integration/lm_control/lm_control_ipc_test.py +++ b/tests/integration/lm_control/lm_control_ipc_test.py @@ -48,7 +48,7 @@ def test_lm_control_ipc(target, setup_test, assert_test_results, remote_test_dir # Start the LM skeleton stub — runs in the background. logger.info("Starting lm_skeleton_stub") - lm_proc = target.execute_async(lm_stub, args=[lm_config], cwd=str(test_dir)) + lm_proc = target.execute_async(lm_stub, args=["--service_instance_manifest", lm_config], cwd=str(test_dir)) # Wait for the skeleton to offer the service. # ILmControl::Create polls for up to 2 s, so 3 s gives ample margin. diff --git a/tests/integration/lm_control/lm_skeleton_stub.cpp b/tests/integration/lm_control/lm_skeleton_stub.cpp index 490eee194..fbdd6256c 100644 --- a/tests/integration/lm_control/lm_skeleton_stub.cpp +++ b/tests/integration/lm_control/lm_skeleton_stub.cpp @@ -14,7 +14,10 @@ #include #include #include +#include +#include #include +#include #include "score/mw/com/runtime.h" #include "score/mw/com/runtime_configuration.h" @@ -22,12 +25,76 @@ namespace { +using score::mw::launch_manager::control::IGraph; +using score::mw::launch_manager::control::LmControlServer; +using score::mw::launch_manager::control::RunTargetRequest; + std::atomic g_stop{false}; void on_signal(int) noexcept { g_stop = true; } + +/// @brief Minimal IGraph that echoes the requested Run Target back as +/// settled, once processRequests() is called from the main loop. +class StubGraph final : public IGraph +{ + public: + void setServer(LmControlServer* server) noexcept + { + server_ = server; + } + + std::optional getActiveRunTarget() override + { + return "Running"; + } + + bool enqueueRunTargetActivation(RunTargetRequest request) override + { + std::lock_guard lock(mutex_); + pending_.push_back(request); + return true; + } + + /// @brief Settle all queued activations. Must be called from the main loop. + /// + /// Simulates a recovery action: activating "FailRunning" is treated as a + /// failure that the graph recovers from by settling on "Recovery" instead, + /// reported with no requestId so activationCompleted attributes it to + /// RunTargetActivationSource::kRecoveryAction. + void processRequests() + { + std::vector requests; + { + std::lock_guard lock(mutex_); + requests.swap(pending_); + } + + for (const auto& request : requests) + { + if (server_ == nullptr) + { + continue; + } + + if (request.runTargetName.as_string_view() == "FailRunning") + { + server_->activationCompleted("Recovery", std::nullopt); + } + else + { + server_->activationCompleted(request.runTargetName.as_string_view(), request.requestId); + } + } + } + + private: + LmControlServer* server_{nullptr}; + std::mutex mutex_; + std::vector pending_; +}; } // namespace /// @brief Minimal binary that starts the LmControlSkeleton (stub handlers) and @@ -45,22 +112,24 @@ int main(int argc, const char* argv[]) std::signal(SIGTERM, on_signal); std::signal(SIGINT, on_signal); - score::mw::com::runtime::InitializeRuntime( - score::mw::com::runtime::RuntimeConfiguration{argv[1]}); + score::mw::com::runtime::InitializeRuntime(argc, argv); + + StubGraph graph; + LmControlServer server{graph}; + graph.setServer(&server); - score::mw::launch_manager::control::LmControlServer server{}; if (!server.Initialize()) { std::cerr << "lm_skeleton_stub: LmControlServer::Initialize failed\n"; return 1; } - std::cout << "lm_skeleton_stub: skeleton ready\n"; - std::cout.flush(); + std::cout << "lm_skeleton_stub: skeleton ready" << std::endl; while (!g_stop) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); + graph.processRequests(); } server.Shutdown(); From 842e16a530994072cf7b73ad4185e846430498f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 14 Aug 2026 15:51:58 +0200 Subject: [PATCH 07/27] Use StartFindService API --- .../lm_control/src/details/ilm_control.cpp | 7 +- .../src/details/lm_control_impl.cpp | 153 +++++++----- .../src/details/lm_control_impl.hpp | 51 ++-- .../lm_control_impl_PollLoop_not_working.cpp | 220 ---------------- .../lm_control_impl_PollLoop_working.cpp | 236 ------------------ ...ol_impl__SetReceiveHandler_not_working.cpp | 210 ---------------- ...ontrol_impl__SetReceiveHandler_working.cpp | 216 ---------------- .../src/lm_control/src/ilm_control.hpp | 3 +- 8 files changed, 124 insertions(+), 972 deletions(-) delete mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_not_working.cpp delete mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_working.cpp delete mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_not_working.cpp delete mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_working.cpp diff --git a/score/launch_manager/src/lm_control/src/details/ilm_control.cpp b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp index 8173bdbfc..1bc251bfb 100644 --- a/score/launch_manager/src/lm_control/src/details/ilm_control.cpp +++ b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp @@ -21,9 +21,12 @@ score::Result> ILmControl::Create( std::string_view instance_specifier) { auto impl = std::make_unique(instance_specifier); - if (!impl->IsConnected()) + // Construction only fails for a malformed specifier or a failed StartFindService; + // a not-yet-available service instance is not an error (discovery continues in + // the background and method calls return kCommunicationError until it appears). + if (const auto init_error = impl->InitError(); init_error.has_value()) { - return score::MakeUnexpected(ExecErrc::kCommunicationError); + return score::MakeUnexpected(init_error.value()); } return impl; } diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp index 168cc8181..c24c244b5 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp @@ -13,8 +13,8 @@ #include "lm_control_impl.hpp" -#include #include +#include // DEBUG helpers — remove once IPC delivery is confirmed stable. #define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl @@ -22,67 +22,78 @@ namespace score::mw::lifecycle { +namespace +{ +constexpr std::size_t kActivationResultSampleCount = 8U; +} // namespace + LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept - : proxy_{} - , callback_{} - , callback_mutex_{} { auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); if (!specifier_result.has_value()) { DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); + init_error_ = ExecErrc::kInvalidArguments; return; } - const auto specifier = std::move(specifier_result).value(); - // Poll FindService until the LM skeleton is available or timeout expires. - const auto deadline = std::chrono::steady_clock::now() + kFindServiceTimeout; - while (std::chrono::steady_clock::now() < deadline) + // Start asynchronous, background service discovery. The handler runs on a + // mw::com thread whenever availability changes; it wires up the proxy on the + // first matching instance. Construction succeeds even if the instance is not + // available yet — discovery keeps running until it appears. + auto start_result = LmControlProxy::StartFindService( + [this](score::mw::com::ServiceHandleContainer handles, + score::mw::com::FindServiceHandle find_handle) noexcept { + OnServiceFound(std::move(handles), find_handle); + }, + std::move(specifier_result).value()); + + if (!start_result.has_value()) { - auto find_result = LmControlProxy::FindService(specifier); - if (find_result.has_value() && !find_result.value().empty()) - { - auto create_result = LmControlProxy::Create(find_result.value().front()); - if (create_result.has_value()) - { - proxy_.emplace(std::move(create_result).value()); - DBG_PROXY("Proxy created successfully"); - break; - } - else - { - DBG_PROXY("Proxy::Create failed"); - } - } - std::this_thread::sleep_for(kFindServiceInterval); + DBG_PROXY("StartFindService failed — no background discovery running"); + init_error_ = ExecErrc::kCommunicationError; + return; } - if (!proxy_.has_value()) + std::lock_guard lock{mutex_}; + // The handler may already have run (and stopped discovery) synchronously + // during StartFindService; only remember the handle if it is still active. + if (!find_service_stopped_) { - DBG_PROXY("FindService timed out — no skeleton found within " << kFindServiceTimeout.count() << "s"); - return; + find_handle_ = start_result.value(); } +} - // Subscribe to activation_result events — max 1 sample buffered. - // DEBUG: verify subscription succeeded and log state — remove once event delivery is confirmed stable. +void LmControlImpl::OnServiceFound(score::mw::com::ServiceHandleContainer handles, + score::mw::com::FindServiceHandle find_handle) noexcept +{ + std::lock_guard lock{mutex_}; + + // Ignore "service went away" notifications and any callbacks after we are set up. + if (handles.empty() || proxy_.has_value()) { - auto subscribe_result = proxy_->activation_result.Subscribe(1U); - if (!subscribe_result.has_value()) - { - DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); - proxy_.reset(); - return; - } + return; + } - const auto state = proxy_->activation_result.GetSubscriptionState(); - const char* state_str = - state == score::mw::com::SubscriptionState::kSubscribed ? "kSubscribed" : - state == score::mw::com::SubscriptionState::kNotSubscribed ? "kNotSubscribed" : - state == score::mw::com::SubscriptionState::kSubscriptionPending ? "kSubscriptionPending" : - "unknown"; - // Expected: kSubscribed. kSubscriptionPending means the skeleton has not - // yet acknowledged — samples may arrive late or not at all. - DBG_PROXY("activation_result subscription state after Subscribe: " << state_str); + auto create_result = LmControlProxy::Create(handles.front()); + if (!create_result.has_value()) + { + DBG_PROXY("Proxy::Create failed"); + return; + } + proxy_.emplace(std::move(create_result).value()); + DBG_PROXY("Proxy created successfully"); + + // Subscribe to activation_result events. Buffer several samples so that + // multiple activations settled back-to-back on the skeleton side are all + // delivered rather than the latest overwriting earlier ones in a single + // consumer slot. Must not exceed the provider's numberOfSampleSlots. + auto subscribe_result = proxy_->activation_result.Subscribe(kActivationResultSampleCount); + if (!subscribe_result.has_value()) + { + DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); + proxy_.reset(); + return; } proxy_->activation_result.SetReceiveHandler([this]() { @@ -90,23 +101,38 @@ LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept }); DBG_PROXY("SetReceiveHandler registered"); - // Looks like the Warmup call is not needed... - // Maybe the problem was sending the event from the - // LmControlServer::OnActivateRunTarget() --> skeleton side - // Maybe we need to send events from a separate thread... - // But why polling is not working??? - // - // Warmup: call GetNewSamples once to signal to the skeleton that this proxy - // is actively reading. This triggers the skeleton to establish its reverse - // notification channel (LoLa skeleton→proxy socket) before the first event - // is sent, so the SetReceiveHandler notification arrives reliably. - //proxy_->activation_result.GetNewSamples( - // [](score::mw::com::SamplePtr) noexcept {}, 1U); - //DBG_PROXY("Warmup GetNewSamples called"); + // First matching instance is wired up — stop the ongoing discovery. + auto stop_result = LmControlProxy::StopFindService(find_handle); + if (!stop_result.has_value()) + { + DBG_PROXY("StopFindService failed"); + } + find_service_stopped_ = true; + + // Publish the fully set-up proxy_ to method-caller threads. + connected_.store(true, std::memory_order_release); } LmControlImpl::~LmControlImpl() noexcept { + std::optional handle_to_stop; + { + std::lock_guard lock{mutex_}; + if (!find_service_stopped_ && find_handle_.has_value()) + { + handle_to_stop = find_handle_; + find_service_stopped_ = true; + } + } + if (handle_to_stop.has_value()) + { + auto stop_result = LmControlProxy::StopFindService(handle_to_stop.value()); + if (!stop_result.has_value()) + { + DBG_PROXY("StopFindService failed during shutdown"); + } + } + if (proxy_.has_value()) { proxy_->activation_result.Unsubscribe(); @@ -116,17 +142,14 @@ LmControlImpl::~LmControlImpl() noexcept score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) { - if (!proxy_.has_value()) + if (!connected_.load(std::memory_order_acquire)) { return score::MakeUnexpected(ExecErrc::kCommunicationError); } DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); - const ActivateRunTargetRequest request{ - runTargetName, - force ? ActivationMode::kForced : ActivationMode::kQueued - }; + const ActivateRunTargetRequest request{runTargetName, force ? ActivationMode::kForced : ActivationMode::kQueued}; auto result = proxy_->activate_run_target(request); if (!result.has_value()) @@ -160,7 +183,7 @@ score::Result LmControlImpl::register_run_target_activation_callback(Activ score::Result LmControlImpl::get_active_run_target() { - if (!proxy_.has_value()) + if (!connected_.load(std::memory_order_acquire)) { return score::MakeUnexpected(ExecErrc::kCommunicationError); } @@ -205,7 +228,7 @@ void LmControlImpl::OnActivationResult() DBG_PROXY("activation_result: no callback registered — sample dropped"); } }, - 1U); + kActivationResultSampleCount); if (get_result.has_value() && get_result.value() > 0U) { diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index 1c400fdc5..93d6d00b7 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -14,11 +14,9 @@ #define SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP #include -#include #include #include #include -#include #include "score/mw/lifecycle/ilm_control.hpp" #include "lm_control_service.h" @@ -29,20 +27,24 @@ namespace score::mw::lifecycle /// @brief Real mw::com proxy-based implementation of ILmControl. /// /// Connects to the Launch Manager's LmControlSkeleton via mw::com SHM transport. -/// Construction attempts service discovery with a configurable timeout — if the -/// LM skeleton is not found within that time, IsConnected() returns false and -/// ILmControl::Create returns kCommunicationError. +/// Construction starts an asynchronous service discovery (StartFindService) that +/// keeps searching for the instance in the background. Construction itself +/// succeeds as long as the instance specifier is valid and StartFindService was +/// accepted; the proxy may only become available later. Until it does, method +/// calls return kCommunicationError. class LmControlImpl final : public ILmControl { public: - static constexpr auto kFindServiceTimeout = std::chrono::seconds(2); - static constexpr auto kFindServiceInterval = std::chrono::milliseconds(100); - explicit LmControlImpl(std::string_view instance_specifier) noexcept; ~LmControlImpl() noexcept override; - /// @brief Returns true if the proxy connected successfully during construction. - bool IsConnected() const noexcept { return proxy_.has_value(); } + /// @brief Error that occurred during construction, or nullopt on success. + /// + /// Populated only when the instance specifier was malformed + /// (kInvalidArguments) or the mw::com StartFindService call failed + /// (kCommunicationError). A missing service instance is not an error here — + /// discovery keeps running in the background. + std::optional InitError() const noexcept { return init_error_; } score::Result activate_run_target(RunTargetName runTargetName, bool force = false) override; @@ -52,23 +54,30 @@ class LmControlImpl final : public ILmControl score::Result get_active_run_target() override; - static constexpr auto kPollingInterval = std::chrono::milliseconds(50); - private: + /// @brief Invoked by mw::com whenever service availability changes. On the + /// first matching instance it creates the proxy, subscribes to + /// activation results, and stops the ongoing discovery. + void OnServiceFound(score::mw::com::ServiceHandleContainer handles, + score::mw::com::FindServiceHandle find_handle) noexcept; void OnActivationResult(); - void PollLoop(); + // Error captured during construction (nullopt on success). + std::optional init_error_; + + // Guards proxy_ setup and the discovery-handle bookkeeping against the + // concurrent StartFindService handler and the destructor. + std::mutex mutex_; std::optional proxy_; + std::optional find_handle_; + bool find_service_stopped_{false}; + + // Publishes the fully set-up proxy_ to method-caller threads. Read with + // acquire ordering in the API methods, set with release once proxy_ is ready. + std::atomic connected_{false}; + ActivationCallback callback_; std::mutex callback_mutex_; - - // TODO: SetReceiveHandler is the intended mechanism for event notifications. - // The skeleton→proxy notification socket is established lazily by the skeleton - // on the first Send call, causing the first notification to be dropped. - // Polling reads directly from SHM and is not affected by this. - // Remove poller once the notification channel initialisation is resolved. - std::atomic stop_polling_{false}; - std::thread poller_; }; } // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_not_working.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_not_working.cpp deleted file mode 100644 index d2387616d..000000000 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_not_working.cpp +++ /dev/null @@ -1,220 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#include "lm_control_impl.hpp" - -#include -#include -#include - -// DEBUG helpers — remove once IPC delivery is confirmed stable. -#define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl - -namespace score::mw::lifecycle -{ - -LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept - : proxy_{} - , callback_{} - , callback_mutex_{} -{ - auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); - if (!specifier_result.has_value()) - { - DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); - return; - } - const auto specifier = std::move(specifier_result).value(); - - // Poll FindService until the LM skeleton is available or timeout expires. - const auto deadline = std::chrono::steady_clock::now() + kFindServiceTimeout; - while (std::chrono::steady_clock::now() < deadline) - { - auto find_result = LmControlProxy::FindService(specifier); - if (find_result.has_value() && !find_result.value().empty()) - { - auto create_result = LmControlProxy::Create(find_result.value().front()); - if (create_result.has_value()) - { - proxy_.emplace(std::move(create_result).value()); - DBG_PROXY("Proxy created successfully"); - break; - } - else - { - DBG_PROXY("Proxy::Create failed"); - } - } - std::this_thread::sleep_for(kFindServiceInterval); - } - - if (!proxy_.has_value()) - { - DBG_PROXY("FindService timed out — no skeleton found within " << kFindServiceTimeout.count() << "s"); - return; - } - - // Subscribe to activation_result events — max 1 sample buffered. - // DEBUG: verify subscription succeeded and log state — remove once event delivery is confirmed stable. - { - auto subscribe_result = proxy_->activation_result.Subscribe(1U); - if (!subscribe_result.has_value()) - { - DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); - proxy_.reset(); - return; - } - - const auto state = proxy_->activation_result.GetSubscriptionState(); - const char* state_str = - state == score::mw::com::SubscriptionState::kSubscribed ? "kSubscribed" : - state == score::mw::com::SubscriptionState::kNotSubscribed ? "kNotSubscribed" : - state == score::mw::com::SubscriptionState::kSubscriptionPending ? "kSubscriptionPending" : - "unknown"; - // Expected: kSubscribed. kSubscriptionPending means the skeleton has not - // yet acknowledged — samples may arrive late or not at all. - DBG_PROXY("activation_result subscription state after Subscribe: " << state_str); - } - - // TODO: SetReceiveHandler is the intended mechanism — replace poller with: - // proxy_->activation_result.SetReceiveHandler([this]() { OnActivationResult(); }); - // The skeleton→proxy notification socket is established lazily on the first - // Send, causing the first notification to be dropped. Polling reads SHM - // directly and is not affected by this timing issue. - poller_ = std::thread([this]() { PollLoop(); }); - DBG_PROXY("Polling thread started (interval: " << kPollingInterval.count() << "ms)"); -} - -LmControlImpl::~LmControlImpl() noexcept -{ - stop_polling_ = true; - if (poller_.joinable()) - { - poller_.join(); - } - if (proxy_.has_value()) - { - proxy_->activation_result.Unsubscribe(); - DBG_PROXY("Unsubscribed and destroyed proxy"); - } -} - -score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) -{ - if (!proxy_.has_value()) - { - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); - - const ActivateRunTargetRequest request{ - runTargetName, - force ? ActivationMode::kForced : ActivationMode::kQueued - }; - - auto result = proxy_->activate_run_target(request); - if (!result.has_value()) - { - DBG_PROXY("activate_run_target: proxy method call failed"); - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - const auto& response = *result.value(); - if (response.status == RequestStatus::kRejected) - { - DBG_PROXY("activate_run_target: rejected by LM"); - return score::MakeUnexpected(response.rejection_reason); - } - - DBG_PROXY("activate_run_target: accepted by LM"); - return {}; -} - -score::Result LmControlImpl::register_run_target_activation_callback(ActivationCallback callback) -{ - if (!callback) - { - return score::MakeUnexpected(ExecErrc::kInvalidArguments); - } - std::lock_guard lock{callback_mutex_}; - callback_ = std::move(callback); - DBG_PROXY("Activation callback registered"); - return {}; -} - -score::Result LmControlImpl::get_active_run_target() -{ - if (!proxy_.has_value()) - { - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - DBG_PROXY("get_active_run_target: calling proxy"); - - auto result = proxy_->get_active_run_target(); - if (!result.has_value()) - { - DBG_PROXY("get_active_run_target: proxy method call failed"); - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - const auto& response = *result.value(); - if (response.status == QueryStatus::kNotAvailable) - { - DBG_PROXY("get_active_run_target: LM reports kActivationInProgress"); - return score::MakeUnexpected(response.rejection_reason); - } - - DBG_PROXY("get_active_run_target: returned '" << response.run_target << "'"); - return response.run_target; -} - -void LmControlImpl::PollLoop() -{ - while (!stop_polling_) - { - OnActivationResult(); - std::this_thread::sleep_for(kPollingInterval); - } -} - -void LmControlImpl::OnActivationResult() -{ - auto get_result = proxy_->activation_result.GetNewSamples( - [this](score::mw::com::SamplePtr sample) noexcept { - DBG_PROXY("activation_result received: run_target='" << sample->activated_run_target << "'"); - ActivationCallback cb; - { - std::lock_guard lock{callback_mutex_}; - cb = callback_; - } - if (cb) - { - cb(sample->activation_source, sample->activated_run_target); - DBG_PROXY("activation_result: user callback invoked"); - } - else - { - DBG_PROXY("activation_result: no callback registered — sample dropped"); - } - }, - 1U); - - if (get_result.has_value() && get_result.value() > 0U) - { - DBG_PROXY("GetNewSamples: processed " << get_result.value() << " sample(s)"); - } -} - -} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_working.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_working.cpp deleted file mode 100644 index 5362aee8d..000000000 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl_PollLoop_working.cpp +++ /dev/null @@ -1,236 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#include "lm_control_impl.hpp" - -#include -#include -#include - -// DEBUG helpers — remove once IPC delivery is confirmed stable. -#define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl - -namespace score::mw::lifecycle -{ - -LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept - : proxy_{} - , callback_{} - , callback_mutex_{} -{ - auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); - if (!specifier_result.has_value()) - { - DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); - return; - } - const auto specifier = std::move(specifier_result).value(); - - // Poll FindService until the LM skeleton is available or timeout expires. - const auto deadline = std::chrono::steady_clock::now() + kFindServiceTimeout; - while (std::chrono::steady_clock::now() < deadline) - { - auto find_result = LmControlProxy::FindService(specifier); - if (find_result.has_value() && !find_result.value().empty()) - { - auto create_result = LmControlProxy::Create(find_result.value().front()); - if (create_result.has_value()) - { - proxy_.emplace(std::move(create_result).value()); - DBG_PROXY("Proxy created successfully"); - break; - } - else - { - DBG_PROXY("Proxy::Create failed"); - } - } - std::this_thread::sleep_for(kFindServiceInterval); - } - - if (!proxy_.has_value()) - { - DBG_PROXY("FindService timed out — no skeleton found within " << kFindServiceTimeout.count() << "s"); - return; - } - - // Subscribe to activation_result events — max 1 sample buffered. - // DEBUG: verify subscription succeeded and log state — remove once event delivery is confirmed stable. - { - auto subscribe_result = proxy_->activation_result.Subscribe(1U); - if (!subscribe_result.has_value()) - { - DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); - proxy_.reset(); - return; - } - - const auto state = proxy_->activation_result.GetSubscriptionState(); - const char* state_str = - state == score::mw::com::SubscriptionState::kSubscribed ? "kSubscribed" : - state == score::mw::com::SubscriptionState::kNotSubscribed ? "kNotSubscribed" : - state == score::mw::com::SubscriptionState::kSubscriptionPending ? "kSubscriptionPending" : - "unknown"; - // Expected: kSubscribed. kSubscriptionPending means the skeleton has not - // yet acknowledged — samples may arrive late or not at all. - DBG_PROXY("activation_result subscription state after Subscribe: " << state_str); - } - - // Drain any stale event left in the SHM slot from a previous subscription. - // With numberOfSampleSlots=1 and no enforceMaxSamples, a delayed Send from - // a previous subscriber's activation can still be in the slot when a new - // subscriber starts. Consuming it here prevents the new subscriber's callback - // from firing with a stale result. - // - // Please also note that consuming an event (the GetNewSamples call) doesn't clear - // the data from the SHM slot. It only advances the read pointer for that specific proxy. - // When somebody creates a brand new proxy and calls Subscribe, it gets a fresh read - // pointer starting from the beginning of the circular buffer. From the new - // proxy's perspective, the Running data in the SHM slot looks like a new, unread event. - // So it will create false callback if the buffer is undrained. - proxy_->activation_result.GetNewSamples( - [](score::mw::com::SamplePtr) noexcept {}, 1U); - DBG_PROXY("Stale event drain done"); - - // TODO: SetReceiveHandler is the intended mechanism — replace poller with: - // proxy_->activation_result.SetReceiveHandler([this]() { OnActivationResult(); }); - // The skeleton→proxy notification socket is established lazily on the first - // Send, causing the first notification to be dropped. Polling reads SHM - // directly and is not affected by this timing issue. - poller_ = std::thread([this]() { PollLoop(); }); - DBG_PROXY("Polling thread started (interval: " << kPollingInterval.count() << "ms)"); -} - -LmControlImpl::~LmControlImpl() noexcept -{ - stop_polling_ = true; - if (poller_.joinable()) - { - poller_.join(); - } - if (proxy_.has_value()) - { - proxy_->activation_result.Unsubscribe(); - DBG_PROXY("Unsubscribed and destroyed proxy"); - } -} - -score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) -{ - if (!proxy_.has_value()) - { - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); - - const ActivateRunTargetRequest request{ - runTargetName, - force ? ActivationMode::kForced : ActivationMode::kQueued - }; - - auto result = proxy_->activate_run_target(request); - if (!result.has_value()) - { - DBG_PROXY("activate_run_target: proxy method call failed"); - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - const auto& response = *result.value(); - if (response.status == RequestStatus::kRejected) - { - DBG_PROXY("activate_run_target: rejected by LM"); - return score::MakeUnexpected(response.rejection_reason); - } - - DBG_PROXY("activate_run_target: accepted by LM"); - return {}; -} - -score::Result LmControlImpl::register_run_target_activation_callback(ActivationCallback callback) -{ - if (!callback) - { - return score::MakeUnexpected(ExecErrc::kInvalidArguments); - } - std::lock_guard lock{callback_mutex_}; - callback_ = std::move(callback); - DBG_PROXY("Activation callback registered"); - return {}; -} - -score::Result LmControlImpl::get_active_run_target() -{ - if (!proxy_.has_value()) - { - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - DBG_PROXY("get_active_run_target: calling proxy"); - - auto result = proxy_->get_active_run_target(); - if (!result.has_value()) - { - DBG_PROXY("get_active_run_target: proxy method call failed"); - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - const auto& response = *result.value(); - if (response.status == QueryStatus::kNotAvailable) - { - DBG_PROXY("get_active_run_target: LM reports kActivationInProgress"); - return score::MakeUnexpected(response.rejection_reason); - } - - DBG_PROXY("get_active_run_target: returned '" << response.run_target << "'"); - return response.run_target; -} - -void LmControlImpl::PollLoop() -{ - while (!stop_polling_) - { - OnActivationResult(); - std::this_thread::sleep_for(kPollingInterval); - } -} - -void LmControlImpl::OnActivationResult() -{ - auto get_result = proxy_->activation_result.GetNewSamples( - [this](score::mw::com::SamplePtr sample) noexcept { - DBG_PROXY("activation_result received: run_target='" << sample->activated_run_target << "'"); - ActivationCallback cb; - { - std::lock_guard lock{callback_mutex_}; - cb = callback_; - } - if (cb) - { - cb(sample->activation_source, sample->activated_run_target); - DBG_PROXY("activation_result: user callback invoked"); - } - else - { - DBG_PROXY("activation_result: no callback registered — sample dropped"); - } - }, - 1U); - - if (get_result.has_value() && get_result.value() > 0U) - { - DBG_PROXY("GetNewSamples: processed " << get_result.value() << " sample(s)"); - } -} - -} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_not_working.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_not_working.cpp deleted file mode 100644 index c91b17cc8..000000000 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_not_working.cpp +++ /dev/null @@ -1,210 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#include "lm_control_impl.hpp" - -#include -#include - -// DEBUG helpers — remove once IPC delivery is confirmed stable. -#define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl - -namespace score::mw::lifecycle -{ - -LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept - : proxy_{} - , callback_{} - , callback_mutex_{} -{ - auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); - if (!specifier_result.has_value()) - { - DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); - return; - } - const auto specifier = std::move(specifier_result).value(); - - // Poll FindService until the LM skeleton is available or timeout expires. - const auto deadline = std::chrono::steady_clock::now() + kFindServiceTimeout; - while (std::chrono::steady_clock::now() < deadline) - { - auto find_result = LmControlProxy::FindService(specifier); - if (find_result.has_value() && !find_result.value().empty()) - { - auto create_result = LmControlProxy::Create(find_result.value().front()); - if (create_result.has_value()) - { - proxy_.emplace(std::move(create_result).value()); - DBG_PROXY("Proxy created successfully"); - break; - } - else - { - DBG_PROXY("Proxy::Create failed"); - } - } - std::this_thread::sleep_for(kFindServiceInterval); - } - - if (!proxy_.has_value()) - { - DBG_PROXY("FindService timed out — no skeleton found within " << kFindServiceTimeout.count() << "s"); - return; - } - - // Subscribe to activation_result events — max 1 sample buffered. - // DEBUG: verify subscription succeeded and log state — remove once event delivery is confirmed stable. - { - auto subscribe_result = proxy_->activation_result.Subscribe(1U); - if (!subscribe_result.has_value()) - { - DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); - proxy_.reset(); - return; - } - - const auto state = proxy_->activation_result.GetSubscriptionState(); - const char* state_str = - state == score::mw::com::SubscriptionState::kSubscribed ? "kSubscribed" : - state == score::mw::com::SubscriptionState::kNotSubscribed ? "kNotSubscribed" : - state == score::mw::com::SubscriptionState::kSubscriptionPending ? "kSubscriptionPending" : - "unknown"; - // Expected: kSubscribed. kSubscriptionPending means the skeleton has not - // yet acknowledged — samples may arrive late or not at all. - DBG_PROXY("activation_result subscription state after Subscribe: " << state_str); - } - - proxy_->activation_result.SetReceiveHandler([this]() { - OnActivationResult(); - }); - DBG_PROXY("SetReceiveHandler registered"); - - // Warmup: call GetNewSamples once to signal to the skeleton that this proxy - // is actively reading. This triggers the skeleton to establish its reverse - // notification channel (LoLa skeleton→proxy socket) before the first event - // is sent, so the SetReceiveHandler notification arrives reliably. - proxy_->activation_result.GetNewSamples( - [](score::mw::com::SamplePtr) noexcept {}, 1U); - DBG_PROXY("Warmup GetNewSamples called"); -} - -LmControlImpl::~LmControlImpl() noexcept -{ - if (proxy_.has_value()) - { - proxy_->activation_result.Unsubscribe(); - DBG_PROXY("Unsubscribed and destroyed proxy"); - } -} - -score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) -{ - if (!proxy_.has_value()) - { - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); - - const ActivateRunTargetRequest request{ - runTargetName, - force ? ActivationMode::kForced : ActivationMode::kQueued - }; - - auto result = proxy_->activate_run_target(request); - if (!result.has_value()) - { - DBG_PROXY("activate_run_target: proxy method call failed"); - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - const auto& response = *result.value(); - if (response.status == RequestStatus::kRejected) - { - DBG_PROXY("activate_run_target: rejected by LM"); - return score::MakeUnexpected(response.rejection_reason); - } - - DBG_PROXY("activate_run_target: accepted by LM"); - return {}; -} - -score::Result LmControlImpl::register_run_target_activation_callback(ActivationCallback callback) -{ - if (!callback) - { - return score::MakeUnexpected(ExecErrc::kInvalidArguments); - } - std::lock_guard lock{callback_mutex_}; - callback_ = std::move(callback); - DBG_PROXY("Activation callback registered"); - return {}; -} - -score::Result LmControlImpl::get_active_run_target() -{ - if (!proxy_.has_value()) - { - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - DBG_PROXY("get_active_run_target: calling proxy"); - - auto result = proxy_->get_active_run_target(); - if (!result.has_value()) - { - DBG_PROXY("get_active_run_target: proxy method call failed"); - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - const auto& response = *result.value(); - if (response.status == QueryStatus::kNotAvailable) - { - DBG_PROXY("get_active_run_target: LM reports kActivationInProgress"); - return score::MakeUnexpected(response.rejection_reason); - } - - DBG_PROXY("get_active_run_target: returned '" << response.run_target << "'"); - return response.run_target; -} - -void LmControlImpl::OnActivationResult() -{ - auto get_result = proxy_->activation_result.GetNewSamples( - [this](score::mw::com::SamplePtr sample) noexcept { - DBG_PROXY("activation_result received: run_target='" << sample->activated_run_target << "'"); - ActivationCallback cb; - { - std::lock_guard lock{callback_mutex_}; - cb = callback_; - } - if (cb) - { - cb(sample->activation_source, sample->activated_run_target); - DBG_PROXY("activation_result: user callback invoked"); - } - else - { - DBG_PROXY("activation_result: no callback registered — sample dropped"); - } - }, - 1U); - - if (get_result.has_value() && get_result.value() > 0U) - { - DBG_PROXY("GetNewSamples: processed " << get_result.value() << " sample(s)"); - } -} - -} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_working.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_working.cpp deleted file mode 100644 index 168cc8181..000000000 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl__SetReceiveHandler_working.cpp +++ /dev/null @@ -1,216 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#include "lm_control_impl.hpp" - -#include -#include - -// DEBUG helpers — remove once IPC delivery is confirmed stable. -#define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl - -namespace score::mw::lifecycle -{ - -LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept - : proxy_{} - , callback_{} - , callback_mutex_{} -{ - auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); - if (!specifier_result.has_value()) - { - DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); - return; - } - const auto specifier = std::move(specifier_result).value(); - - // Poll FindService until the LM skeleton is available or timeout expires. - const auto deadline = std::chrono::steady_clock::now() + kFindServiceTimeout; - while (std::chrono::steady_clock::now() < deadline) - { - auto find_result = LmControlProxy::FindService(specifier); - if (find_result.has_value() && !find_result.value().empty()) - { - auto create_result = LmControlProxy::Create(find_result.value().front()); - if (create_result.has_value()) - { - proxy_.emplace(std::move(create_result).value()); - DBG_PROXY("Proxy created successfully"); - break; - } - else - { - DBG_PROXY("Proxy::Create failed"); - } - } - std::this_thread::sleep_for(kFindServiceInterval); - } - - if (!proxy_.has_value()) - { - DBG_PROXY("FindService timed out — no skeleton found within " << kFindServiceTimeout.count() << "s"); - return; - } - - // Subscribe to activation_result events — max 1 sample buffered. - // DEBUG: verify subscription succeeded and log state — remove once event delivery is confirmed stable. - { - auto subscribe_result = proxy_->activation_result.Subscribe(1U); - if (!subscribe_result.has_value()) - { - DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); - proxy_.reset(); - return; - } - - const auto state = proxy_->activation_result.GetSubscriptionState(); - const char* state_str = - state == score::mw::com::SubscriptionState::kSubscribed ? "kSubscribed" : - state == score::mw::com::SubscriptionState::kNotSubscribed ? "kNotSubscribed" : - state == score::mw::com::SubscriptionState::kSubscriptionPending ? "kSubscriptionPending" : - "unknown"; - // Expected: kSubscribed. kSubscriptionPending means the skeleton has not - // yet acknowledged — samples may arrive late or not at all. - DBG_PROXY("activation_result subscription state after Subscribe: " << state_str); - } - - proxy_->activation_result.SetReceiveHandler([this]() { - OnActivationResult(); - }); - DBG_PROXY("SetReceiveHandler registered"); - - // Looks like the Warmup call is not needed... - // Maybe the problem was sending the event from the - // LmControlServer::OnActivateRunTarget() --> skeleton side - // Maybe we need to send events from a separate thread... - // But why polling is not working??? - // - // Warmup: call GetNewSamples once to signal to the skeleton that this proxy - // is actively reading. This triggers the skeleton to establish its reverse - // notification channel (LoLa skeleton→proxy socket) before the first event - // is sent, so the SetReceiveHandler notification arrives reliably. - //proxy_->activation_result.GetNewSamples( - // [](score::mw::com::SamplePtr) noexcept {}, 1U); - //DBG_PROXY("Warmup GetNewSamples called"); -} - -LmControlImpl::~LmControlImpl() noexcept -{ - if (proxy_.has_value()) - { - proxy_->activation_result.Unsubscribe(); - DBG_PROXY("Unsubscribed and destroyed proxy"); - } -} - -score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) -{ - if (!proxy_.has_value()) - { - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); - - const ActivateRunTargetRequest request{ - runTargetName, - force ? ActivationMode::kForced : ActivationMode::kQueued - }; - - auto result = proxy_->activate_run_target(request); - if (!result.has_value()) - { - DBG_PROXY("activate_run_target: proxy method call failed"); - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - const auto& response = *result.value(); - if (response.status == RequestStatus::kRejected) - { - DBG_PROXY("activate_run_target: rejected by LM"); - return score::MakeUnexpected(response.rejection_reason); - } - - DBG_PROXY("activate_run_target: accepted by LM"); - return {}; -} - -score::Result LmControlImpl::register_run_target_activation_callback(ActivationCallback callback) -{ - if (!callback) - { - return score::MakeUnexpected(ExecErrc::kInvalidArguments); - } - std::lock_guard lock{callback_mutex_}; - callback_ = std::move(callback); - DBG_PROXY("Activation callback registered"); - return {}; -} - -score::Result LmControlImpl::get_active_run_target() -{ - if (!proxy_.has_value()) - { - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - DBG_PROXY("get_active_run_target: calling proxy"); - - auto result = proxy_->get_active_run_target(); - if (!result.has_value()) - { - DBG_PROXY("get_active_run_target: proxy method call failed"); - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - const auto& response = *result.value(); - if (response.status == QueryStatus::kNotAvailable) - { - DBG_PROXY("get_active_run_target: LM reports kActivationInProgress"); - return score::MakeUnexpected(response.rejection_reason); - } - - DBG_PROXY("get_active_run_target: returned '" << response.run_target << "'"); - return response.run_target; -} - -void LmControlImpl::OnActivationResult() -{ - auto get_result = proxy_->activation_result.GetNewSamples( - [this](score::mw::com::SamplePtr sample) noexcept { - DBG_PROXY("activation_result received: run_target='" << sample->activated_run_target << "'"); - ActivationCallback cb; - { - std::lock_guard lock{callback_mutex_}; - cb = callback_; - } - if (cb) - { - cb(sample->activation_source, sample->activated_run_target); - DBG_PROXY("activation_result: user callback invoked"); - } - else - { - DBG_PROXY("activation_result: no callback registered — sample dropped"); - } - }, - 1U); - - if (get_result.has_value() && get_result.value() > 0U) - { - DBG_PROXY("GetNewSamples: processed " << get_result.value() << " sample(s)"); - } -} - -} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/ilm_control.hpp b/score/launch_manager/src/lm_control/src/ilm_control.hpp index 8427a088c..fc2f1fae7 100644 --- a/score/launch_manager/src/lm_control/src/ilm_control.hpp +++ b/score/launch_manager/src/lm_control/src/ilm_control.hpp @@ -66,7 +66,7 @@ class ILmControl using ActivationCallback = std::function; - /// @brief Factory method - create a connected ILmControl instance. + /// @brief Factory method - create a ILmControl instance. /// /// @warning **Work in progress API shape not yet finalised.** /// The signature and behaviour of this method may change. Open @@ -89,7 +89,6 @@ class ILmControl /// @returns A unique_ptr to the ILmControl instance on success. /// /// @error kInvalidArguments instance_specifier is empty or malformed. - /// @error kCommunicationError The Launch Manager service could not be reached. static score::Result> Create(std::string_view instance_specifier); /// @brief Virtual destructor for safe deletion through this interface. From 4a8decbf4a37554d88a1d08e06ef8724d5d2c977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 14 Aug 2026 16:05:40 +0200 Subject: [PATCH 08/27] Use mw::log macros --- score/launch_manager/src/lm_control/BUILD | 1 + .../lm_control/src/details/ilm_control.cpp | 9 +-- .../src/details/lm_control_impl.cpp | 75 +++++++++---------- .../src/details/lm_control_impl.hpp | 29 ++++--- 4 files changed, 51 insertions(+), 63 deletions(-) diff --git a/score/launch_manager/src/lm_control/BUILD b/score/launch_manager/src/lm_control/BUILD index 00cfbd958..0d3bd648c 100644 --- a/score/launch_manager/src/lm_control/BUILD +++ b/score/launch_manager/src/lm_control/BUILD @@ -46,6 +46,7 @@ cc_library( ], deps = [ "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/common:log", "@score_baselibs//score/result", "@score_communication//score/mw/com", ], diff --git a/score/launch_manager/src/lm_control/src/details/ilm_control.cpp b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp index 1bc251bfb..3548017fc 100644 --- a/score/launch_manager/src/lm_control/src/details/ilm_control.cpp +++ b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp @@ -17,14 +17,11 @@ namespace score::mw::lifecycle { -score::Result> ILmControl::Create( - std::string_view instance_specifier) +score::Result> ILmControl::Create(std::string_view instance_specifier) { auto impl = std::make_unique(instance_specifier); - // Construction only fails for a malformed specifier or a failed StartFindService; - // a not-yet-available service instance is not an error (discovery continues in - // the background and method calls return kCommunicationError until it appears). - if (const auto init_error = impl->InitError(); init_error.has_value()) + const auto init_error = impl->getInitError(); + if (init_error) { return score::MakeUnexpected(init_error.value()); } diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp index c24c244b5..68b0bfe21 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp @@ -13,11 +13,9 @@ #include "lm_control_impl.hpp" -#include #include -// DEBUG helpers — remove once IPC delivery is confirmed stable. -#define DBG_PROXY(msg) std::cout << "[LmControlImpl DEBUG] " << msg << std::endl +#include "score/mw/launch_manager/common/log.hpp" namespace score::mw::lifecycle { @@ -32,25 +30,22 @@ LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); if (!specifier_result.has_value()) { - DBG_PROXY("InstanceSpecifier::Create failed for: " << instance_specifier); + LM_LOG_ERROR() << "LmControlImpl: InstanceSpecifier::Create failed for: " << instance_specifier; init_error_ = ExecErrc::kInvalidArguments; return; } - // Start asynchronous, background service discovery. The handler runs on a - // mw::com thread whenever availability changes; it wires up the proxy on the - // first matching instance. Construction succeeds even if the instance is not - // available yet — discovery keeps running until it appears. auto start_result = LmControlProxy::StartFindService( - [this](score::mw::com::ServiceHandleContainer handles, - score::mw::com::FindServiceHandle find_handle) noexcept { - OnServiceFound(std::move(handles), find_handle); + [this]( + score::mw::com::ServiceHandleContainer handles, + score::mw::com::FindServiceHandle find_handle) noexcept { + onServiceFound(std::move(handles), find_handle); }, std::move(specifier_result).value()); if (!start_result.has_value()) { - DBG_PROXY("StartFindService failed — no background discovery running"); + LM_LOG_ERROR() << "LmControlImpl: StartFindService failed — no background discovery running"; init_error_ = ExecErrc::kCommunicationError; return; } @@ -64,8 +59,9 @@ LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept } } -void LmControlImpl::OnServiceFound(score::mw::com::ServiceHandleContainer handles, - score::mw::com::FindServiceHandle find_handle) noexcept +void LmControlImpl::onServiceFound( + score::mw::com::ServiceHandleContainer handles, + score::mw::com::FindServiceHandle find_handle) noexcept { std::lock_guard lock{mutex_}; @@ -78,34 +74,30 @@ void LmControlImpl::OnServiceFound(score::mw::com::ServiceHandleContaineractivation_result.Subscribe(kActivationResultSampleCount); if (!subscribe_result.has_value()) { - DBG_PROXY("activation_result.Subscribe failed — GetNewSamples will always return nothing"); + LM_LOG_ERROR() << "LmControlImpl: activation_result.Subscribe failed — GetNewSamples will always return nothing"; proxy_.reset(); return; } proxy_->activation_result.SetReceiveHandler([this]() { - OnActivationResult(); + onActivationResult(); }); - DBG_PROXY("SetReceiveHandler registered"); + LM_LOG_DEBUG() << "LmControlImpl: SetReceiveHandler registered"; // First matching instance is wired up — stop the ongoing discovery. auto stop_result = LmControlProxy::StopFindService(find_handle); if (!stop_result.has_value()) { - DBG_PROXY("StopFindService failed"); + LM_LOG_ERROR() << "LmControlImpl: StopFindService failed"; } find_service_stopped_ = true; @@ -120,7 +112,7 @@ LmControlImpl::~LmControlImpl() noexcept std::lock_guard lock{mutex_}; if (!find_service_stopped_ && find_handle_.has_value()) { - handle_to_stop = find_handle_; + handle_to_stop = find_handle_; find_service_stopped_ = true; } } @@ -129,14 +121,14 @@ LmControlImpl::~LmControlImpl() noexcept auto stop_result = LmControlProxy::StopFindService(handle_to_stop.value()); if (!stop_result.has_value()) { - DBG_PROXY("StopFindService failed during shutdown"); + LM_LOG_ERROR() << "LmControlImpl: StopFindService failed during shutdown"; } } if (proxy_.has_value()) { proxy_->activation_result.Unsubscribe(); - DBG_PROXY("Unsubscribed and destroyed proxy"); + LM_LOG_DEBUG() << "LmControlImpl: unsubscribed and destroyed proxy"; } } @@ -147,25 +139,25 @@ score::Result LmControlImpl::activate_run_target(RunTargetName runTargetNa return score::MakeUnexpected(ExecErrc::kCommunicationError); } - DBG_PROXY("activate_run_target: " << runTargetName << " force=" << force); + LM_LOG_DEBUG() << "LmControlImpl: activate_run_target: " << runTargetName << " force=" << force; const ActivateRunTargetRequest request{runTargetName, force ? ActivationMode::kForced : ActivationMode::kQueued}; auto result = proxy_->activate_run_target(request); if (!result.has_value()) { - DBG_PROXY("activate_run_target: proxy method call failed"); + LM_LOG_ERROR() << "LmControlImpl: activate_run_target: proxy method call failed"; return score::MakeUnexpected(ExecErrc::kCommunicationError); } const auto& response = *result.value(); if (response.status == RequestStatus::kRejected) { - DBG_PROXY("activate_run_target: rejected by LM"); + LM_LOG_DEBUG() << "LmControlImpl: activate_run_target: rejected by LM"; return score::MakeUnexpected(response.rejection_reason); } - DBG_PROXY("activate_run_target: accepted by LM"); + LM_LOG_DEBUG() << "LmControlImpl: activate_run_target: accepted by LM"; return {}; } @@ -177,7 +169,7 @@ score::Result LmControlImpl::register_run_target_activation_callback(Activ } std::lock_guard lock{callback_mutex_}; callback_ = std::move(callback); - DBG_PROXY("Activation callback registered"); + LM_LOG_DEBUG() << "LmControlImpl: activation callback registered"; return {}; } @@ -188,31 +180,32 @@ score::Result LmControlImpl::get_active_run_target() return score::MakeUnexpected(ExecErrc::kCommunicationError); } - DBG_PROXY("get_active_run_target: calling proxy"); + LM_LOG_DEBUG() << "LmControlImpl: get_active_run_target: calling proxy"; auto result = proxy_->get_active_run_target(); if (!result.has_value()) { - DBG_PROXY("get_active_run_target: proxy method call failed"); + LM_LOG_ERROR() << "LmControlImpl: get_active_run_target: proxy method call failed"; return score::MakeUnexpected(ExecErrc::kCommunicationError); } const auto& response = *result.value(); if (response.status == QueryStatus::kNotAvailable) { - DBG_PROXY("get_active_run_target: LM reports kActivationInProgress"); + LM_LOG_DEBUG() << "LmControlImpl: get_active_run_target: LM reports kActivationInProgress"; return score::MakeUnexpected(response.rejection_reason); } - DBG_PROXY("get_active_run_target: returned '" << response.run_target << "'"); + LM_LOG_DEBUG() << "LmControlImpl: get_active_run_target: returned '" << response.run_target << "'"; return response.run_target; } -void LmControlImpl::OnActivationResult() +void LmControlImpl::onActivationResult() { auto get_result = proxy_->activation_result.GetNewSamples( [this](score::mw::com::SamplePtr sample) noexcept { - DBG_PROXY("activation_result received: run_target='" << sample->activated_run_target << "'"); + LM_LOG_DEBUG() << "LmControlImpl: activation_result received: run_target='" + << sample->activated_run_target << "'"; ActivationCallback cb; { std::lock_guard lock{callback_mutex_}; @@ -221,18 +214,18 @@ void LmControlImpl::OnActivationResult() if (cb) { cb(sample->activation_source, sample->activated_run_target); - DBG_PROXY("activation_result: user callback invoked"); + LM_LOG_DEBUG() << "LmControlImpl: activation_result: user callback invoked"; } else { - DBG_PROXY("activation_result: no callback registered — sample dropped"); + LM_LOG_WARN() << "LmControlImpl: activation_result: no callback registered — sample dropped"; } }, kActivationResultSampleCount); if (get_result.has_value() && get_result.value() > 0U) { - DBG_PROXY("GetNewSamples: processed " << get_result.value() << " sample(s)"); + LM_LOG_DEBUG() << "LmControlImpl: GetNewSamples: processed " << get_result.value() << " sample(s)"; } } diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index 93d6d00b7..f1e3c7ea4 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -18,15 +18,15 @@ #include #include -#include "score/mw/lifecycle/ilm_control.hpp" #include "lm_control_service.h" +#include "score/mw/lifecycle/ilm_control.hpp" namespace score::mw::lifecycle { -/// @brief Real mw::com proxy-based implementation of ILmControl. +/// @brief mw::com proxy-based implementation of ILmControl. /// -/// Connects to the Launch Manager's LmControlSkeleton via mw::com SHM transport. +/// Connects to the Launch Manager's LmControlSkeleton via mw::com. /// Construction starts an asynchronous service discovery (StartFindService) that /// keeps searching for the instance in the background. Construction itself /// succeeds as long as the instance specifier is valid and StartFindService was @@ -39,18 +39,14 @@ class LmControlImpl final : public ILmControl ~LmControlImpl() noexcept override; /// @brief Error that occurred during construction, or nullopt on success. - /// - /// Populated only when the instance specifier was malformed - /// (kInvalidArguments) or the mw::com StartFindService call failed - /// (kCommunicationError). A missing service instance is not an error here — - /// discovery keeps running in the background. - std::optional InitError() const noexcept { return init_error_; } + std::optional getInitError() const noexcept + { + return init_error_; + } - score::Result activate_run_target(RunTargetName runTargetName, - bool force = false) override; + score::Result activate_run_target(RunTargetName runTargetName, bool force = false) override; - score::Result register_run_target_activation_callback( - ActivationCallback callback) override; + score::Result register_run_target_activation_callback(ActivationCallback callback) override; score::Result get_active_run_target() override; @@ -58,9 +54,10 @@ class LmControlImpl final : public ILmControl /// @brief Invoked by mw::com whenever service availability changes. On the /// first matching instance it creates the proxy, subscribes to /// activation results, and stops the ongoing discovery. - void OnServiceFound(score::mw::com::ServiceHandleContainer handles, - score::mw::com::FindServiceHandle find_handle) noexcept; - void OnActivationResult(); + void onServiceFound( + score::mw::com::ServiceHandleContainer handles, + score::mw::com::FindServiceHandle find_handle) noexcept; + void onActivationResult(); // Error captured during construction (nullopt on success). std::optional init_error_; From 59c9f3efcda09b44105473912c5f1a1e01d0ff9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 14 Aug 2026 16:06:19 +0200 Subject: [PATCH 09/27] Extend integration test --- .../lm_control/lm_control_ipc_test.cpp | 32 ++++++++++++++++++- .../lm_control/lm_mw_com_config.json | 4 +-- .../lm_control/lm_skeleton_stub.cpp | 12 +++---- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/tests/integration/lm_control/lm_control_ipc_test.cpp b/tests/integration/lm_control/lm_control_ipc_test.cpp index ab24c8db7..c09f9634d 100644 --- a/tests/integration/lm_control/lm_control_ipc_test.cpp +++ b/tests/integration/lm_control/lm_control_ipc_test.cpp @@ -37,7 +37,7 @@ class LmControlIPCTest : public ::testing::Test std::unique_ptr lm_; }; -TEST_F(LmControlIPCTest, CallingAPI) +TEST_F(LmControlIPCTest, LmControlAPI) { TEST_STEP("GetActiveRunTarget returns Running") { @@ -89,6 +89,36 @@ TEST_F(LmControlIPCTest, CallingAPI) EXPECT_EQ(name.value(), "Recovery"); EXPECT_EQ(source.value(), RunTargetActivationSource::kRecoveryAction); } + + TEST_STEP("Can call ActivateRunTarget multiple times and get multiple callbacks") + { + std::vector names; + std::vector sources; + std::promise sync_promise; + auto future = sync_promise.get_future(); + + lm_->register_run_target_activation_callback( + [&sync_promise, &names, &sources](RunTargetActivationSource s, RunTargetName n) { + sources.push_back(s); + names.push_back(n); + if (names.size() == 2) + { + sync_promise.set_value(); + } + }); + + auto result1 = lm_->activate_run_target("Running1"); + ASSERT_TRUE(result1.has_value()); + + auto result2 = lm_->activate_run_target("Running2"); + ASSERT_TRUE(result2.has_value()); + + ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); + EXPECT_EQ(names[0], "Running1"); + EXPECT_EQ(sources[0], RunTargetActivationSource::kStateManagerRequest); + EXPECT_EQ(names[1], "Running2"); + EXPECT_EQ(sources[1], RunTargetActivationSource::kStateManagerRequest); + } } } // namespace score::mw::lifecycle diff --git a/tests/integration/lm_control/lm_mw_com_config.json b/tests/integration/lm_control/lm_mw_com_config.json index 9d84dd451..f309c5481 100644 --- a/tests/integration/lm_control/lm_mw_com_config.json +++ b/tests/integration/lm_control/lm_mw_com_config.json @@ -46,8 +46,8 @@ "events": [ { "eventName": "ActivationResult", - "numberOfSampleSlots": 1, - "maxSubscribers": 5 + "numberOfSampleSlots": 8, + "maxSubscribers": 1 } ], "methods": [ diff --git a/tests/integration/lm_control/lm_skeleton_stub.cpp b/tests/integration/lm_control/lm_skeleton_stub.cpp index fbdd6256c..9842517f0 100644 --- a/tests/integration/lm_control/lm_skeleton_stub.cpp +++ b/tests/integration/lm_control/lm_skeleton_stub.cpp @@ -60,10 +60,10 @@ class StubGraph final : public IGraph /// @brief Settle all queued activations. Must be called from the main loop. /// - /// Simulates a recovery action: activating "FailRunning" is treated as a - /// failure that the graph recovers from by settling on "Recovery" instead, - /// reported with no requestId so activationCompleted attributes it to - /// RunTargetActivationSource::kRecoveryAction. + /// Simulates a recovery action: activating a Run Target whose name starts + /// with "Fail" is treated as a failure that the graph recovers from by + /// settling on "Recovery" instead, reported with no requestId so + /// activationCompleted attributes it to RunTargetActivationSource::kRecoveryAction. void processRequests() { std::vector requests; @@ -79,7 +79,7 @@ class StubGraph final : public IGraph continue; } - if (request.runTargetName.as_string_view() == "FailRunning") + if (request.runTargetName.as_string_view().rfind("Fail", 0) == 0) { server_->activationCompleted("Recovery", std::nullopt); } @@ -105,7 +105,7 @@ int main(int argc, const char* argv[]) { if (argc < 2) { - std::cerr << "Usage: lm_skeleton_stub \n"; + std::cerr << "Usage: lm_skeleton_stub --service_instance_manifest \n"; return 1; } From 9868b591fee1eef20326ce45b448374fb27bb004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Sun, 16 Aug 2026 15:26:01 +0200 Subject: [PATCH 10/27] LmControlImpl rework and UT --- score/launch_manager/src/lm_control/BUILD | 45 +- .../lm_control/src/details/ilm_control.cpp | 11 +- .../lm_control/src/details/ilm_control_UT.cpp | 85 +++ .../src/details/lm_control_impl.cpp | 232 ------ .../src/details/lm_control_impl.hpp | 392 ++++++++++- .../src/details/lm_control_impl_UT.cpp | 660 ++++++++++++++++++ .../src/details/test_lm_mw_com_config.json | 100 +++ .../src/lm_control/src/ilm_control.hpp | 2 +- .../launch_manager/src/lm_control/tests/BUILD | 31 + .../src/lm_control/tests/lm_control_CT.cpp | 385 ++++++++++ .../lm_control/tests/lm_mw_com_config.json | 100 +++ 11 files changed, 1778 insertions(+), 265 deletions(-) create mode 100644 score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp delete mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp create mode 100644 score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp create mode 100644 score/launch_manager/src/lm_control/src/details/test_lm_mw_com_config.json create mode 100644 score/launch_manager/src/lm_control/tests/BUILD create mode 100644 score/launch_manager/src/lm_control/tests/lm_control_CT.cpp create mode 100644 score/launch_manager/src/lm_control/tests/lm_mw_com_config.json diff --git a/score/launch_manager/src/lm_control/BUILD b/score/launch_manager/src/lm_control/BUILD index 0d3bd648c..507fba94a 100644 --- a/score/launch_manager/src/lm_control/BUILD +++ b/score/launch_manager/src/lm_control/BUILD @@ -10,7 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_cc//cc:defs.bzl", "cc_library") +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") cc_library( name = "lm_control_service", @@ -29,11 +29,10 @@ cc_library( name = "lm_control", srcs = [ "src/details/ilm_control.cpp", - "src/details/lm_control_impl.cpp", - "src/details/lm_control_impl.hpp", - "src/details/lm_control_service.h", ], hdrs = [ + "src/details/lm_control_impl.hpp", + "src/details/lm_control_service.h", "src/fixed_string.hpp", "src/ilm_control.hpp", "src/run_target_activation_source.hpp", @@ -52,3 +51,41 @@ cc_library( ], ) +# --------------------------------------------------------------------------- +# Unit test: BasicLmControlImpl driven through a fake proxy injected at compile +# time. Lives next to the implementation it covers. No mw::com runtime, shared +# memory, or manifest required. +# --------------------------------------------------------------------------- + +cc_test( + name = "lm_control_impl_UT", + srcs = ["src/details/lm_control_impl_UT.cpp"], + deps = [ + ":lm_control", + "//score/launch_manager:error", + "@googletest//:gtest_main", + "@score_baselibs//score/result", + "@score_communication//score/mw/com", + ], +) + +# --------------------------------------------------------------------------- +# Unit test: ILmControl::Create factory. Runs against a mw::com runtime +# initialized from the shared manifest; no provider skeleton is required. +# --------------------------------------------------------------------------- + +cc_test( + name = "ilm_control_UT", + srcs = ["src/details/ilm_control_UT.cpp"], + args = [ + "--service_instance_manifest", + "$(location src/details/test_lm_mw_com_config.json)", + ], + data = ["src/details/test_lm_mw_com_config.json"], + deps = [ + ":lm_control", + "@googletest//:gtest", + "@score_communication//score/mw/com", + ], +) + diff --git a/score/launch_manager/src/lm_control/src/details/ilm_control.cpp b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp index 3548017fc..5d08821f2 100644 --- a/score/launch_manager/src/lm_control/src/details/ilm_control.cpp +++ b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp @@ -12,20 +12,19 @@ ********************************************************************************/ #include "score/mw/lifecycle/ilm_control.hpp" -#include "lm_control_impl.hpp" +#include "score/mw/lifecycle/details/lm_control_impl.hpp" namespace score::mw::lifecycle { score::Result> ILmControl::Create(std::string_view instance_specifier) { - auto impl = std::make_unique(instance_specifier); - const auto init_error = impl->getInitError(); - if (init_error) + auto impl_result = LmControlImpl::Create(instance_specifier); + if (!impl_result.has_value()) { - return score::MakeUnexpected(init_error.value()); + return score::MakeUnexpected>(impl_result.error()); } - return impl; + return std::move(impl_result).value(); } } // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp b/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp new file mode 100644 index 000000000..e8e97955c --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp @@ -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 + ********************************************************************************/ + +/// @file +/// @brief Unit test for the ILmControl::Create factory in ilm_control.cpp. +/// +/// ILmControl::Create builds the production BasicLmControlImpl wired to the real +/// mw::com proxy, so there is no compile-time seam here. The test therefore runs +/// against a mw::com runtime initialized from the manifest passed on the command +/// line via --service_instance_manifest (see main()); a valid specifier only +/// needs StartFindService to succeed (background discovery), so no provider +/// skeleton is required for Create() to hand back a usable instance. + +#include + +#include +#include + +#include "score/mw/com/runtime.h" +#include "score/mw/lifecycle/ilm_control.hpp" + +namespace score::mw::lifecycle +{ +namespace +{ + +// Present in the test manifest (lm_mw_com_config.json). +constexpr std::string_view kValidSpecifier = "StateManager/LaunchManager/Instance"; + +class ILmControlUT : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + + // The following two equivalence classes are tested here: + // * Return a valid instance + // * Return an error + // The different error cases are tested in the underlying impl class LmControlImpl unit tests + } +}; + +TEST_F(ILmControlUT, ReturnsValidInstance) +{ + RecordProperty("Description", "ILmControl::Create returns a valid instance for a valid instance specifier."); + + auto result = ILmControl::Create(std::string{kValidSpecifier}); + + ASSERT_TRUE(result.has_value()); + EXPECT_NE(result.value(), nullptr); +} + +TEST_F(ILmControlUT, ReturnsInitErrorForInvalidSpecifier) +{ + RecordProperty( + "Description", "ILmControl::Create returns kInvalidArguments error for an invalid instance specifier."); + + auto result = ILmControl::Create(""); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kInvalidArguments); +} + +} // namespace +} // namespace score::mw::lifecycle + +/// Usage: ilm_control_UT --service_instance_manifest +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + score::mw::com::runtime::InitializeRuntime(argc, const_cast(argv)); + return RUN_ALL_TESTS(); +} diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp deleted file mode 100644 index 68b0bfe21..000000000 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.cpp +++ /dev/null @@ -1,232 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#include "lm_control_impl.hpp" - -#include - -#include "score/mw/launch_manager/common/log.hpp" - -namespace score::mw::lifecycle -{ - -namespace -{ -constexpr std::size_t kActivationResultSampleCount = 8U; -} // namespace - -LmControlImpl::LmControlImpl(std::string_view instance_specifier) noexcept -{ - auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); - if (!specifier_result.has_value()) - { - LM_LOG_ERROR() << "LmControlImpl: InstanceSpecifier::Create failed for: " << instance_specifier; - init_error_ = ExecErrc::kInvalidArguments; - return; - } - - auto start_result = LmControlProxy::StartFindService( - [this]( - score::mw::com::ServiceHandleContainer handles, - score::mw::com::FindServiceHandle find_handle) noexcept { - onServiceFound(std::move(handles), find_handle); - }, - std::move(specifier_result).value()); - - if (!start_result.has_value()) - { - LM_LOG_ERROR() << "LmControlImpl: StartFindService failed — no background discovery running"; - init_error_ = ExecErrc::kCommunicationError; - return; - } - - std::lock_guard lock{mutex_}; - // The handler may already have run (and stopped discovery) synchronously - // during StartFindService; only remember the handle if it is still active. - if (!find_service_stopped_) - { - find_handle_ = start_result.value(); - } -} - -void LmControlImpl::onServiceFound( - score::mw::com::ServiceHandleContainer handles, - score::mw::com::FindServiceHandle find_handle) noexcept -{ - std::lock_guard lock{mutex_}; - - // Ignore "service went away" notifications and any callbacks after we are set up. - if (handles.empty() || proxy_.has_value()) - { - return; - } - - auto create_result = LmControlProxy::Create(handles.front()); - if (!create_result.has_value()) - { - LM_LOG_ERROR() << "LmControlImpl: Proxy::Create failed"; - return; - } - proxy_.emplace(std::move(create_result).value()); - LM_LOG_INFO() << "LmControlImpl: proxy created successfully"; - - auto subscribe_result = proxy_->activation_result.Subscribe(kActivationResultSampleCount); - if (!subscribe_result.has_value()) - { - LM_LOG_ERROR() << "LmControlImpl: activation_result.Subscribe failed — GetNewSamples will always return nothing"; - proxy_.reset(); - return; - } - - proxy_->activation_result.SetReceiveHandler([this]() { - onActivationResult(); - }); - LM_LOG_DEBUG() << "LmControlImpl: SetReceiveHandler registered"; - - // First matching instance is wired up — stop the ongoing discovery. - auto stop_result = LmControlProxy::StopFindService(find_handle); - if (!stop_result.has_value()) - { - LM_LOG_ERROR() << "LmControlImpl: StopFindService failed"; - } - find_service_stopped_ = true; - - // Publish the fully set-up proxy_ to method-caller threads. - connected_.store(true, std::memory_order_release); -} - -LmControlImpl::~LmControlImpl() noexcept -{ - std::optional handle_to_stop; - { - std::lock_guard lock{mutex_}; - if (!find_service_stopped_ && find_handle_.has_value()) - { - handle_to_stop = find_handle_; - find_service_stopped_ = true; - } - } - if (handle_to_stop.has_value()) - { - auto stop_result = LmControlProxy::StopFindService(handle_to_stop.value()); - if (!stop_result.has_value()) - { - LM_LOG_ERROR() << "LmControlImpl: StopFindService failed during shutdown"; - } - } - - if (proxy_.has_value()) - { - proxy_->activation_result.Unsubscribe(); - LM_LOG_DEBUG() << "LmControlImpl: unsubscribed and destroyed proxy"; - } -} - -score::Result LmControlImpl::activate_run_target(RunTargetName runTargetName, bool force) -{ - if (!connected_.load(std::memory_order_acquire)) - { - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - LM_LOG_DEBUG() << "LmControlImpl: activate_run_target: " << runTargetName << " force=" << force; - - const ActivateRunTargetRequest request{runTargetName, force ? ActivationMode::kForced : ActivationMode::kQueued}; - - auto result = proxy_->activate_run_target(request); - if (!result.has_value()) - { - LM_LOG_ERROR() << "LmControlImpl: activate_run_target: proxy method call failed"; - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - const auto& response = *result.value(); - if (response.status == RequestStatus::kRejected) - { - LM_LOG_DEBUG() << "LmControlImpl: activate_run_target: rejected by LM"; - return score::MakeUnexpected(response.rejection_reason); - } - - LM_LOG_DEBUG() << "LmControlImpl: activate_run_target: accepted by LM"; - return {}; -} - -score::Result LmControlImpl::register_run_target_activation_callback(ActivationCallback callback) -{ - if (!callback) - { - return score::MakeUnexpected(ExecErrc::kInvalidArguments); - } - std::lock_guard lock{callback_mutex_}; - callback_ = std::move(callback); - LM_LOG_DEBUG() << "LmControlImpl: activation callback registered"; - return {}; -} - -score::Result LmControlImpl::get_active_run_target() -{ - if (!connected_.load(std::memory_order_acquire)) - { - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - LM_LOG_DEBUG() << "LmControlImpl: get_active_run_target: calling proxy"; - - auto result = proxy_->get_active_run_target(); - if (!result.has_value()) - { - LM_LOG_ERROR() << "LmControlImpl: get_active_run_target: proxy method call failed"; - return score::MakeUnexpected(ExecErrc::kCommunicationError); - } - - const auto& response = *result.value(); - if (response.status == QueryStatus::kNotAvailable) - { - LM_LOG_DEBUG() << "LmControlImpl: get_active_run_target: LM reports kActivationInProgress"; - return score::MakeUnexpected(response.rejection_reason); - } - - LM_LOG_DEBUG() << "LmControlImpl: get_active_run_target: returned '" << response.run_target << "'"; - return response.run_target; -} - -void LmControlImpl::onActivationResult() -{ - auto get_result = proxy_->activation_result.GetNewSamples( - [this](score::mw::com::SamplePtr sample) noexcept { - LM_LOG_DEBUG() << "LmControlImpl: activation_result received: run_target='" - << sample->activated_run_target << "'"; - ActivationCallback cb; - { - std::lock_guard lock{callback_mutex_}; - cb = callback_; - } - if (cb) - { - cb(sample->activation_source, sample->activated_run_target); - LM_LOG_DEBUG() << "LmControlImpl: activation_result: user callback invoked"; - } - else - { - LM_LOG_WARN() << "LmControlImpl: activation_result: no callback registered — sample dropped"; - } - }, - kActivationResultSampleCount); - - if (get_result.has_value() && get_result.value() > 0U) - { - LM_LOG_DEBUG() << "LmControlImpl: GetNewSamples: processed " << get_result.value() << " sample(s)"; - } -} - -} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index f1e3c7ea4..b9803251f 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -14,59 +14,399 @@ #define SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP #include +#include +#include #include #include +#include #include +#include -#include "lm_control_service.h" +#include "score/mw/launch_manager/common/log.hpp" +#include "score/mw/lifecycle/details/lm_control_service.h" #include "score/mw/lifecycle/ilm_control.hpp" namespace score::mw::lifecycle { +/// @brief Translates a RunTargetActivationSource to a human-readable string for logging. +constexpr std::string_view toStringView(const RunTargetActivationSource source) noexcept +{ + switch (source) + { + case RunTargetActivationSource::kStateManagerRequest: + return "StateManagerRequest"; + case RunTargetActivationSource::kRecoveryAction: + return "RecoveryAction"; + } + return "Unknown"; +} + +/// @brief Layer of indirection for mw::com usage +/// @details This allows to inject a fake proxy in tests, while the production code uses the real mw::com proxy. +struct MwComProxyTraits +{ + using Proxy = LmControlProxy; + using HandleType = LmControlProxy::HandleType; + using FindServiceHandle = score::mw::com::FindServiceHandle; + + template + static score::Result StartFindService( + Handler&& handler, + score::mw::com::InstanceSpecifier specifier) + { + return Proxy::StartFindService(std::forward(handler), std::move(specifier)); + } + + static score::Result StopFindService(FindServiceHandle find_handle) + { + return Proxy::StopFindService(find_handle); + } + + static score::Result Create(HandleType handle) + { + return Proxy::Create(handle); + } +}; + /// @brief mw::com proxy-based implementation of ILmControl. /// -/// Connects to the Launch Manager's LmControlSkeleton via mw::com. -/// Construction starts an asynchronous service discovery (StartFindService) that +/// Connects to the Launch Manager via mw::com. +/// Construction starts an asynchronous service discovery that /// keeps searching for the instance in the background. Construction itself -/// succeeds as long as the instance specifier is valid and StartFindService was -/// accepted; the proxy may only become available later. Until it does, method -/// calls return kCommunicationError. -class LmControlImpl final : public ILmControl +/// succeeds as long as the instance specifier is valid and there is no error +// setting up the service discovery; the proxy may only become available later. +// Until it does, method calls return kCommunicationError. +/// +/// @tparam Traits Binds the class to a proxy implementation. Defaults to +/// MwComProxyTraits (the real mw::com proxy). Tests inject a +/// traits type whose Proxy is a fake, giving full control +/// for testing. +template +class BasicLmControlImpl final : public ILmControl { public: - explicit LmControlImpl(std::string_view instance_specifier) noexcept; - ~LmControlImpl() noexcept override; + /// @brief Create a fully initialized instance, or return the construction error. + /// @param[in] instance_specifier The mw::com instance specifier identifying + /// the Launch Manager service. Must be a + /// non-empty, valid specifier string. An empty + /// or malformed value yields kInvalidArguments. + /// @return The ready-to-use instance, or the error that prevented setup. + static score::Result> Create(std::string_view instance_specifier) noexcept + { + auto instance = std::make_unique(instance_specifier); + if (instance->init_error_.has_value()) + { + return score::MakeUnexpected(instance->init_error_.value()); + } + return instance; + } - /// @brief Error that occurred during construction, or nullopt on success. - std::optional getInitError() const noexcept + /// @brief Construct an instance and start asynchronous service discovery. + /// @note Prefer Create(): This constructor is + /// public only so std::make_unique can build the object inside Create(). + explicit BasicLmControlImpl(std::string_view instance_specifier) noexcept { - return init_error_; + auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); + if (!specifier_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: Invalid instance specifier" << instance_specifier + << "Error: " << specifier_result.error(); + init_error_ = ExecErrc::kInvalidArguments; + return; + } + + auto start_result = Traits::StartFindService( + [this](score::mw::com::ServiceHandleContainer handles, FindServiceHandle find_handle) noexcept { + onServiceFound(std::move(handles), find_handle); + }, + std::move(specifier_result).value()); + + if (!start_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: StartFindService failed with error" << start_result.error(); + init_error_ = ExecErrc::kCommunicationError; + return; + } + + std::lock_guard lock{mutex_}; + // The handler may already have run (and stopped discovery) synchronously + // during StartFindService; only remember the handle if it is still active. + if (!find_service_stopped_) + { + find_handle_ = start_result.value(); + } + } + + ~BasicLmControlImpl() noexcept override + { + std::optional handle_to_stop; + { + std::lock_guard lock{mutex_}; + if (!find_service_stopped_ && find_handle_.has_value()) + { + handle_to_stop = find_handle_; + find_service_stopped_ = true; + } + } + if (handle_to_stop.has_value()) + { + auto stop_result = Traits::StopFindService(handle_to_stop.value()); + if (!stop_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: StopFindService failed during shutdown with error:" + << stop_result.error(); + } + } + + if (proxy_.has_value()) + { + // Unsubscribe also unsets the receive handler, no need for explicit UnsetReceiveHandler() call. + proxy_->activation_result.Unsubscribe(); + } } - score::Result activate_run_target(RunTargetName runTargetName, bool force = false) override; + score::Result activate_run_target(RunTargetName runTargetName, bool force = false) override + { + if (!connected_.load(std::memory_order_acquire)) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + LM_LOG_DEBUG() << "LmControl: activate_run_target:" << runTargetName << " force=" << force; + + const ActivateRunTargetRequest request{ + runTargetName, force ? ActivationMode::kForced : ActivationMode::kQueued}; + + auto result = proxy_->activate_run_target(request); + if (!result.has_value()) + { + LM_LOG_ERROR() << "LmControl: activate_run_target: proxy method call failed with error:" << result.error(); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == RequestStatus::kRejected) + { + LM_LOG_DEBUG() << "LmControl: activate_run_target: rejected by LM with code" << response.rejection_reason; + return score::MakeUnexpected(response.rejection_reason); + } + + return {}; + } + + score::Result register_run_target_activation_callback(ActivationCallback callback) override + { + if (!callback) + { + return score::MakeUnexpected(ExecErrc::kInvalidArguments); + } + std::lock_guard lock{callback_mutex_}; + callback_ = std::move(callback); + return {}; + } + + score::Result get_active_run_target() override + { + if (!connected_.load(std::memory_order_acquire)) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } - score::Result register_run_target_activation_callback(ActivationCallback callback) override; + auto result = proxy_->get_active_run_target(); + if (!result.has_value()) + { + LM_LOG_ERROR() << "LmControl: get_active_run_target: proxy method call failed with error:" + << result.error(); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } - score::Result get_active_run_target() override; + const auto& response = *result.value(); + if (response.status == QueryStatus::kNotAvailable) + { + LM_LOG_DEBUG() << "LmControl: get_active_run_target: LM reports kActivationInProgress"; + return score::MakeUnexpected(response.rejection_reason); + } + + LM_LOG_DEBUG() << "LmControl: get_active_run_target:" << response.run_target; + return response.run_target; + } private: + using ProxyType = typename Traits::Proxy; + using HandleType = typename Traits::HandleType; + using FindServiceHandle = typename Traits::FindServiceHandle; + + /// @brief Allow to read small number of samples at once + static constexpr std::size_t kActivationResultSampleCount = 4U; + /// @brief Invoked by mw::com whenever service availability changes. On the /// first matching instance it creates the proxy, subscribes to /// activation results, and stops the ongoing discovery. void onServiceFound( - score::mw::com::ServiceHandleContainer handles, - score::mw::com::FindServiceHandle find_handle) noexcept; - void onActivationResult(); + score::mw::com::ServiceHandleContainer handles, + FindServiceHandle find_handle) noexcept + { + std::lock_guard lock{mutex_}; + + // Ignore "service went away" notifications and any callbacks after we are set up. + if (handles.empty() || proxy_.has_value()) + { + return; + } + + if (!createProxy(handles.front())) + { + return; + } + + if (!subscribeToActivationResults()) + { + return; + } + + // First matching instance is wired up — stop the ongoing discovery. + stopDiscovery(find_handle); + + // Publish the fully set-up proxy_ to method-caller threads. + connected_.store(true, std::memory_order_release); + } + + /// @brief Create the proxy for the discovered instance and publish it into proxy_. + /// @pre Caller holds mutex_. + /// @return true if the proxy was created, false on error (proxy_ left empty). + bool createProxy(const HandleType& handle) noexcept + { + auto create_result = Traits::Create(handle); + if (!create_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: Proxy::Create failed with error:" << create_result.error(); + return false; + } + proxy_.emplace(std::move(create_result).value()); + LM_LOG_INFO() << "LmControl: proxy created successfully"; + return true; + } + + /// @brief Subscribe to the activation_result event and install the receive handler. + /// @pre Caller holds mutex_ and proxy_ has a value. + /// @return true on success; on failure proxy_ is reset so we stay unconnected. + bool subscribeToActivationResults() noexcept + { + auto subscribe_result = proxy_->activation_result.Subscribe(kActivationResultSampleCount); + if (!subscribe_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: activation_result.Subscribe failed with error:" << subscribe_result.error(); + proxy_.reset(); + return false; + } + + proxy_->activation_result.SetReceiveHandler([this]() { + onActivationResult(); + }); + return true; + } + + /// @brief Stop the ongoing service discovery now that an instance is wired up. + /// @pre Caller holds mutex_. + void stopDiscovery(const FindServiceHandle& find_handle) noexcept + { + auto stop_result = Traits::StopFindService(find_handle); + if (!stop_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: StopFindService failed with error:" << stop_result.error(); + } + find_service_stopped_ = true; + } + + /// @brief mw::com receive handler for the activation_result event. + /// @details mw::com may invoke this concurrently with itself. Since proxy-event + /// API calls must not run concurrently on the same event, the handler + /// is serialized against itself + void onActivationResult() + { + std::lock_guard processing_lock{sample_processing_mutex_}; + + // First get the number of samples available. + // This way we can have a bounded loop to retrieve all samples. + // Relying purely on the return of GetNewSamples() would yield a possibly unbounded loop. + auto available_result = proxy_->activation_result.GetNumNewSamplesAvailable(); + if (!available_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: GetNumNewSamplesAvailable failed with error:" << available_result.error(); + return; + } + + readActivationEvents(available_result.value()); + } + + /// @brief Reads and forwards up to pending_samples_count pending activation-result samples. + /// @pre The caller holds sample_processing_mutex_. + void readActivationEvents(std::size_t pending_samples_count) + { + std::size_t remaining = pending_samples_count; + std::size_t processed = 0U; + while (remaining > 0U) + { + auto get_result = proxy_->activation_result.GetNewSamples( + [this](auto sample) noexcept { + forwardSample(sample); + }, + remaining); + if (!get_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: GetNewSamples failed with error:" << get_result.error(); + break; + } + + const auto received = get_result.value(); + if (received == 0U) + { + // Nothing retrievable despite a positive snapshot — stop rather + // than spin; any arriving new samples re-trigger the handler. + break; + } + + processed += received; + remaining -= std::min(remaining, received); + } + + LM_LOG_DEBUG() << "LmControl: GetNewSamples: processed" << processed << "sample(s)"; + } + + /// @brief Forwards a single activation-result sample to the registered callback. + /// @details The sample parameter is intentionally a template: in production it + /// is a score::mw::com::SamplePtr, while a fake proxy + /// in tests can hand in any pointer-like sample. Nothing here depends + /// on the concrete mw::com sample type. + template + void forwardSample(const SamplePtrType& sample) noexcept + { + LM_LOG_DEBUG() << "LmControl: activation_result received: run_target=" << sample->activated_run_target + << "source=" << toStringView(sample->activation_source); + ActivationCallback cb; + { + std::lock_guard lock{callback_mutex_}; + cb = callback_; + } + if (cb) + { + cb(sample->activation_source, sample->activated_run_target); + LM_LOG_DEBUG() << "LmControl: activation_result: user callback invoked"; + } + else + { + LM_LOG_WARN() << "LmControl: activation_result: no callback registered — sample dropped"; + } + } // Error captured during construction (nullopt on success). std::optional init_error_; - // Guards proxy_ setup and the discovery-handle bookkeeping against the - // concurrent StartFindService handler and the destructor. + // Guards proxy_ setup and the service discovery handles. std::mutex mutex_; - std::optional proxy_; - std::optional find_handle_; + std::optional proxy_; + std::optional find_handle_; bool find_service_stopped_{false}; // Publishes the fully set-up proxy_ to method-caller threads. Read with @@ -75,8 +415,16 @@ class LmControlImpl final : public ILmControl ActivationCallback callback_; std::mutex callback_mutex_; + + // Serializes onActivationResult() against itself: mw::com may dispatch the + // receive handler concurrently, but proxy-event API calls on a single event + // must not overlap. + std::mutex sample_processing_mutex_; }; +/// @brief Production alias: BasicLmControlImpl wired to the real mw::com proxy. +using LmControlImpl = BasicLmControlImpl; + } // namespace score::mw::lifecycle #endif // SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp new file mode 100644 index 000000000..8ec2efa1e --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp @@ -0,0 +1,660 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "score/mw/lifecycle/details/lm_control_impl.hpp" + +namespace score::mw::lifecycle +{ +namespace +{ + +using ::testing::_; +using ::testing::AllOf; +using ::testing::DoAll; +using ::testing::Field; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SaveArg; + +// Opaque stand-ins for the mw::com discovery handle types. +struct FakeHandle +{ +}; +struct FakeFindHandle +{ +}; + +using FindHandler = std::function, FakeFindHandle)>; + +/// @brief The single mock all framework calls are routed to. Owned by the test. +class MwComMock +{ + public: + MOCK_METHOD(score::Result, StartFindService, (FindHandler handler)); + MOCK_METHOD(score::Result, StopFindService, ()); + MOCK_METHOD(score::Result, CreateProxy, ()); + + MOCK_METHOD(score::Result, Subscribe, (std::size_t max_sample_count)); + MOCK_METHOD(void, SetReceiveHandler, (std::function handler)); + MOCK_METHOD(void, Unsubscribe, ()); + MOCK_METHOD(score::Result, GetNumNewSamplesAvailable, ()); + MOCK_METHOD((score::Result>), GetNewSamples, (std::size_t max_count)); + + MOCK_METHOD(score::Result, ActivateRunTarget, (const ActivateRunTargetRequest& request)); + MOCK_METHOD(score::Result, GetActiveRunTarget, ()); +}; + +/// @brief Delegating adapter for LmControlProxy::activation_result. +struct FakeActivationEvent +{ + MwComMock* mock; + + score::Result Subscribe(std::size_t max_sample_count) + { + return mock->Subscribe(max_sample_count); + } + + void SetReceiveHandler(std::function handler) + { + mock->SetReceiveHandler(std::move(handler)); + } + + void Unsubscribe() + { + mock->Unsubscribe(); + } + + score::Result GetNumNewSamplesAvailable() + { + return mock->GetNumNewSamplesAvailable(); + } + + template + score::Result GetNewSamples(Callback&& callback, std::size_t max_count) + { + auto samples_result = mock->GetNewSamples(max_count); + if (!samples_result.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + auto samples = std::move(samples_result).value(); + for (auto& sample : samples) + { + callback(&sample); + } + return samples.size(); + } +}; + +/// @brief Delegating adapter for LmControlProxy. +struct FakeProxy +{ + MwComMock* mock; + FakeActivationEvent activation_result; + + explicit FakeProxy(MwComMock* m) : mock{m}, activation_result{m} + { + } + + score::Result> activate_run_target(const ActivateRunTargetRequest& req) + { + auto result = mock->ActivateRunTarget(req); + if (!result.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + return std::make_shared(std::move(result).value()); + } + + score::Result> get_active_run_target() + { + auto result = mock->GetActiveRunTarget(); + if (!result.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + return std::make_shared(std::move(result).value()); + } +}; + +/// @brief Compile-time seam to inject the fake proxy into BasicLmControlImpl and to mock mw::com API calls. +struct FakeProxyTraits +{ + using Proxy = FakeProxy; + using HandleType = FakeHandle; + using FindServiceHandle = FakeFindHandle; + + static inline MwComMock* mock{nullptr}; + + template + static score::Result StartFindService(Handler&& handler, score::mw::com::InstanceSpecifier) + { + return mock->StartFindService(FindHandler{std::forward(handler)}); + } + + static score::Result StopFindService(FindServiceHandle) + { + return mock->StopFindService(); + } + + static score::Result Create(HandleType) + { + auto result = mock->CreateProxy(); + if (!result.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + return FakeProxy{mock}; + } +}; + +using LmControlImplType = BasicLmControlImpl; + +constexpr std::string_view kValidSpecifier = "StateManager/LaunchManager/Instance"; + +ActivateRunTargetResponse Accepted() +{ + return ActivateRunTargetResponse{RequestStatus::kAccepted, ExecErrc::kGeneralError}; +} + +GetActiveRunTargetResponse Available(RunTargetName name) +{ + return GetActiveRunTargetResponse{QueryStatus::kAvailable, name, ExecErrc::kGeneralError}; +} + +class LmControlUT : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + + FakeProxyTraits::mock = &mock_; + + // Sensible defaults so the "connected and healthy" path needs no per-test + // wiring; tests override only the calls they care about. + ON_CALL(mock_, StartFindService(_)) + .WillByDefault(DoAll(SaveArg<0>(&find_handler_), Return(score::Result{FakeFindHandle{}}))); + ON_CALL(mock_, StopFindService()).WillByDefault(Return(score::Result{})); + ON_CALL(mock_, CreateProxy()).WillByDefault(Return(score::Result{})); + ON_CALL(mock_, Subscribe(_)).WillByDefault(Return(score::Result{})); + ON_CALL(mock_, SetReceiveHandler(_)).WillByDefault(SaveArg<0>(&receive_handler_)); + ON_CALL(mock_, GetNumNewSamplesAvailable()).WillByDefault(Return(score::Result{0U})); + ON_CALL(mock_, GetNewSamples(_)).WillByDefault(Invoke([](std::size_t) { + return score::Result>{std::vector{}}; + })); + ON_CALL(mock_, ActivateRunTarget(_)).WillByDefault(Invoke([](const ActivateRunTargetRequest&) { + return score::Result{Accepted()}; + })); + ON_CALL(mock_, GetActiveRunTarget()).WillByDefault(Invoke([] { + return score::Result{Available(RunTargetName{"Running"})}; + })); + } + + void TearDown() override + { + FakeProxyTraits::mock = nullptr; + } + + /// @brief Simulate mw::com discovering one matching instance. + void ConnectService() + { + ASSERT_TRUE(find_handler_) << "StartFindService was never called"; + find_handler_(score::mw::com::ServiceHandleContainer{FakeHandle{}}, FakeFindHandle{}); + } + + /// @brief Build a successfully-constructed (but not yet connected) SUT. + std::unique_ptr MakeLmControl() + { + auto result = LmControlImplType::Create(kValidSpecifier); + EXPECT_TRUE(result.has_value()); + return std::move(result).value(); + } + + /// @brief Build an already-connected SUT using the fixture defaults. + std::unique_ptr MakeConnected() + { + auto sut = MakeLmControl(); + ConnectService(); + return sut; + } + + NiceMock mock_; + FindHandler find_handler_{}; + std::function receive_handler_{}; +}; + +// --------------------------------------------------------------------------- +// Construction / discovery lifecycle +// --------------------------------------------------------------------------- + +TEST_F(LmControlUT, InvalidSpecifierYieldsInitError) +{ + RecordProperty( + "Description", + "Create() with an empty instance specifier returns kInvalidArguments and never starts discovery."); + + EXPECT_CALL(mock_, StartFindService(_)).Times(0); + + auto result = LmControlImplType::Create(""); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kInvalidArguments); +} + +TEST_F(LmControlUT, StartFindServiceFailureYieldsInitError) +{ + RecordProperty("Description", "When StartFindService fails, Create() returns kCommunicationError."); + + EXPECT_CALL(mock_, StartFindService(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + + auto result = LmControlImplType::Create(kValidSpecifier); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +TEST_F(LmControlUT, ConstructionStartsBackgroundDiscovery) +{ + RecordProperty( + "Description", "Create() with a valid specifier starts asynchronous service discovery via StartFindService."); + + EXPECT_CALL(mock_, StartFindService(_)); + + auto result = LmControlImplType::Create(kValidSpecifier); + EXPECT_TRUE(result.has_value()); + EXPECT_TRUE(find_handler_); +} + +TEST_F(LmControlUT, MethodsFailWhileNotYetConnected) +{ + RecordProperty( + "Description", + "Before the service is discovered, activate_run_target and get_active_run_target return " + "kCommunicationError without issuing any proxy calls."); + + EXPECT_CALL(mock_, ActivateRunTarget(_)).Times(0); + EXPECT_CALL(mock_, GetActiveRunTarget()).Times(0); + + auto sut = MakeLmControl(); + + auto activate = sut->activate_run_target("Running"); + ASSERT_FALSE(activate.has_value()); + EXPECT_EQ(activate.error(), ExecErrc::kCommunicationError); + + auto get = sut->get_active_run_target(); + ASSERT_FALSE(get.has_value()); + EXPECT_EQ(get.error(), ExecErrc::kCommunicationError); +} + +TEST_F(LmControlUT, DiscoverySubscribesSetsHandlerAndStopsFinding) +{ + RecordProperty( + "Description", + "On discovering the service, the SUT creates the proxy, subscribes to activation results, sets the " + "receive handler, and stops discovery."); + + EXPECT_CALL(mock_, CreateProxy()); + EXPECT_CALL(mock_, Subscribe(4U)); // must match kActivationResultSampleCount + EXPECT_CALL(mock_, SetReceiveHandler(_)); + EXPECT_CALL(mock_, StopFindService()); + + auto sut = MakeConnected(); + EXPECT_TRUE(receive_handler_); +} + +TEST_F(LmControlUT, EmptyHandlesAreIgnored) +{ + RecordProperty( + "Description", "A discovery callback with an empty handle container is ignored and no proxy is created."); + + EXPECT_CALL(mock_, CreateProxy()).Times(0); + + auto sut = MakeLmControl(); + ASSERT_TRUE(find_handler_); + find_handler_(score::mw::com::ServiceHandleContainer{}, FakeFindHandle{}); + + EXPECT_FALSE(sut->get_active_run_target().has_value()); +} + +TEST_F(LmControlUT, SecondDiscoveryCallbackIsIgnored) +{ + RecordProperty( + "Description", "A second discovery callback after the proxy is already set up does not create another proxy."); + + EXPECT_CALL(mock_, CreateProxy()).Times(1); + + auto sut = MakeConnected(); + // A second notification after setup must not create another proxy. + find_handler_(score::mw::com::ServiceHandleContainer{FakeHandle{}}, FakeFindHandle{}); +} + +TEST_F(LmControlUT, SynchronousDiscoveryDuringConstructionStopsFindingOnce) +{ + RecordProperty( + "Description", + "When discovery completes synchronously during construction, discovery is stopped exactly once and " + "not again in the destructor."); + + EXPECT_CALL(mock_, StartFindService(_)).WillOnce(Invoke([](FindHandler handler) { + handler(score::mw::com::ServiceHandleContainer{FakeHandle{}}, FakeFindHandle{}); + return score::Result{FakeFindHandle{}}; + })); + // Discovery is stopped exactly once (during construction), never again in the + // destructor, because construction observed find_service_stopped_ == true. + EXPECT_CALL(mock_, StopFindService()).Times(1); + + auto sut = MakeLmControl(); + EXPECT_TRUE(sut->get_active_run_target().has_value()); +} + +TEST_F(LmControlUT, SubscribeFailureLeavesInstanceDisconnected) +{ + RecordProperty( + "Description", + "When Subscribe fails during discovery, the instance stays disconnected and method calls return " + "kCommunicationError."); + + EXPECT_CALL(mock_, Subscribe(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + + auto sut = MakeConnected(); + + auto get = sut->get_active_run_target(); + ASSERT_FALSE(get.has_value()); + EXPECT_EQ(get.error(), ExecErrc::kCommunicationError); +} + +TEST_F(LmControlUT, ProxyCreateFailureLeavesInstanceDisconnected) +{ + RecordProperty( + "Description", + "When proxy creation fails during discovery, no subscription is attempted and the instance stays " + "disconnected."); + + EXPECT_CALL(mock_, CreateProxy()).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + EXPECT_CALL(mock_, Subscribe(_)).Times(0); + + auto sut = MakeConnected(); + + EXPECT_FALSE(sut->get_active_run_target().has_value()); +} + +TEST_F(LmControlUT, StopFindServiceFailureOnConnectIsTolerated) +{ + RecordProperty( + "Description", + "A StopFindService failure on connect is only logged; the instance still becomes fully connected."); + + EXPECT_CALL(mock_, StopFindService()).WillOnce(Return(score::MakeUnexpected(ExecErrc::kInvalidArguments))); + + auto sut = MakeConnected(); + + // The failure is only logged; the instance is fully connected. + EXPECT_TRUE(sut->get_active_run_target().has_value()); +} + +// --------------------------------------------------------------------------- +// activate_run_target +// --------------------------------------------------------------------------- + +TEST_F(LmControlUT, ActivateForwardsNameAndQueuedModeByDefault) +{ + RecordProperty( + "Description", "activate_run_target forwards the run target name and defaults to queued activation mode."); + + auto sut = MakeConnected(); + + EXPECT_CALL( + mock_, + ActivateRunTarget(AllOf( + Field(&ActivateRunTargetRequest::run_target_name, RunTargetName{"Driving"}), + Field(&ActivateRunTargetRequest::mode, ActivationMode::kQueued)))) + .WillOnce(Return(Accepted())); + + EXPECT_TRUE(sut->activate_run_target("Driving").has_value()); +} + +TEST_F(LmControlUT, ActivateForcedMapsToForcedMode) +{ + RecordProperty("Description", "activate_run_target with force=true maps to forced activation mode."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, ActivateRunTarget(Field(&ActivateRunTargetRequest::mode, ActivationMode::kForced))) + .WillOnce(Return(Accepted())); + + EXPECT_TRUE(sut->activate_run_target("Driving", /*force=*/true).has_value()); +} + +TEST_F(LmControlUT, ActivateRejectionSurfacesRejectionReason) +{ + RecordProperty("Description", "A rejected activation surfaces the rejection reason as the returned error."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, ActivateRunTarget(_)) + .WillOnce(Return(ActivateRunTargetResponse{RequestStatus::kRejected, ExecErrc::kRequestQueueIsFull})); + + auto result = sut->activate_run_target("Driving"); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kRequestQueueIsFull); +} + +TEST_F(LmControlUT, ActivateTransportFailureReturnsCommunicationError) +{ + RecordProperty("Description", "A transport failure during activate_run_target is reported as kCommunicationError."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, ActivateRunTarget(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kFailed))); + + auto result = sut->activate_run_target("Driving"); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +// --------------------------------------------------------------------------- +// get_active_run_target +// --------------------------------------------------------------------------- + +TEST_F(LmControlUT, GetActiveRunTargetReturnsName) +{ + RecordProperty("Description", "get_active_run_target returns the currently active run target name."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetActiveRunTarget()).WillOnce(Return(Available(RunTargetName{"Running"}))); + + auto result = sut->get_active_run_target(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "Running"); +} + +TEST_F(LmControlUT, GetActiveRunTargetNotAvailableSurfacesReason) +{ + RecordProperty( + "Description", + "When no active run target is available, get_active_run_target surfaces the reason as the error."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetActiveRunTarget()) + .WillOnce(Return( + GetActiveRunTargetResponse{QueryStatus::kNotAvailable, RunTargetName{}, ExecErrc::kActivationInProgress})); + + auto result = sut->get_active_run_target(); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kActivationInProgress); +} + +TEST_F(LmControlUT, GetActiveRunTargetTransportFailureReturnsCommunicationError) +{ + RecordProperty( + "Description", "A transport failure during get_active_run_target is reported as kCommunicationError."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetActiveRunTarget()).WillOnce(Return(score::MakeUnexpected(ExecErrc::kFailed))); + + auto result = sut->get_active_run_target(); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +// --------------------------------------------------------------------------- +// register_run_target_activation_callback + activation_result dispatch +// --------------------------------------------------------------------------- + +TEST_F(LmControlUT, RegisterEmptyCallbackRejected) +{ + RecordProperty("Description", "Registering a null activation callback is rejected with kInvalidArguments."); + + auto sut = MakeLmControl(); + auto result = sut->register_run_target_activation_callback(nullptr); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kInvalidArguments); +} + +TEST_F(LmControlUT, ActivationResultInvokesRegisteredCallback) +{ + RecordProperty( + "Description", + "A received activation_result sample is forwarded to the registered callback with its source and " + "run target."); + + auto sut = MakeConnected(); + + std::optional source{}; + std::optional name{}; + ASSERT_TRUE(sut->register_run_target_activation_callback([&](RunTargetActivationSource s, RunTargetName n) { + source = s; + name = n; + }) + .has_value()); + + EXPECT_CALL(mock_, GetNumNewSamplesAvailable()).WillOnce(Return(score::Result{1U})); + EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Invoke([](std::size_t) { + std::vector samples; + samples.push_back(ActivationResult{RunTargetName{"Recovery"}, RunTargetActivationSource::kRecoveryAction}); + return score::Result>{std::move(samples)}; + })); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); + + ASSERT_TRUE(name.has_value()); + EXPECT_EQ(name.value(), "Recovery"); + EXPECT_EQ(source.value(), RunTargetActivationSource::kRecoveryAction); +} + +TEST_F(LmControlUT, ActivationResultWithoutCallbackIsDroppedSafely) +{ + RecordProperty( + "Description", "An activation_result received with no registered callback is dropped without crashing."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetNumNewSamplesAvailable()).WillOnce(Return(score::Result{1U})); + EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Invoke([](std::size_t) { + std::vector samples; + samples.push_back(ActivationResult{RunTargetName{"Running"}, RunTargetActivationSource::kStateManagerRequest}); + return score::Result>{std::move(samples)}; + })); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); // no callback registered — must not crash +} + +TEST_F(LmControlUT, ActivationResultGetNewSamplesFailureIsTolerated) +{ + RecordProperty( + "Description", "A GetNewSamples failure while draining activation results is tolerated without crashing."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetNumNewSamplesAvailable()).WillOnce(Return(score::Result{1U})); + EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); // error branch — must not crash +} + +TEST_F(LmControlUT, LatestRegisteredCallbackWins) +{ + RecordProperty("Description", "The most recently registered activation callback replaces any earlier ones."); + + auto sut = MakeConnected(); + + int first_calls{0}; + int second_calls{0}; + ASSERT_TRUE(sut->register_run_target_activation_callback([&](RunTargetActivationSource, RunTargetName) { + ++first_calls; + }) + .has_value()); + ASSERT_TRUE(sut->register_run_target_activation_callback([&](RunTargetActivationSource, RunTargetName) { + ++second_calls; + }) + .has_value()); + + EXPECT_CALL(mock_, GetNumNewSamplesAvailable()).WillOnce(Return(score::Result{1U})); + EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Invoke([](std::size_t) { + std::vector samples; + samples.push_back(ActivationResult{RunTargetName{"Running"}, RunTargetActivationSource::kStateManagerRequest}); + return score::Result>{std::move(samples)}; + })); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); + + EXPECT_EQ(first_calls, 0); + EXPECT_EQ(second_calls, 1); +} + +// --------------------------------------------------------------------------- +// Destruction +// --------------------------------------------------------------------------- + +TEST_F(LmControlUT, DestructorStopsDiscoveryWhenNeverConnected) +{ + RecordProperty("Description", "Destroying a never-connected instance stops discovery and does not unsubscribe."); + + EXPECT_CALL(mock_, StopFindService()).Times(1); + EXPECT_CALL(mock_, Unsubscribe()).Times(0); + + { + auto sut = MakeLmControl(); + } +} + +TEST_F(LmControlUT, DestructorUnsubscribesWhenConnected) +{ + RecordProperty("Description", "Destroying a connected instance unsubscribes from the activation_result event."); + + EXPECT_CALL(mock_, Unsubscribe()).Times(1); + + MakeConnected().reset(); +} + +} // namespace +} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/test_lm_mw_com_config.json b/score/launch_manager/src/lm_control/src/details/test_lm_mw_com_config.json new file mode 100644 index 000000000..a478be5d7 --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/test_lm_mw_com_config.json @@ -0,0 +1,100 @@ +{ + "serviceTypes": [ + { + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "bindings": [ + { + "binding": "SHM", + "serviceId": 7101, + "events": [ + { + "eventName": "ActivationResult", + "eventId": 1 + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "methodId": 2 + }, + { + "methodName": "GetActiveRunTarget", + "methodId": 3 + } + ] + } + ] + } + ], + "serviceInstances": [ + { + "instanceSpecifier": "LaunchManager/StateManager/Instance", + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "ActivationResult", + "numberOfSampleSlots": 8, + "maxSubscribers": 1 + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "queueSize": 1 + }, + { + "methodName": "GetActiveRunTarget", + "queueSize": 1 + } + ] + } + ] + }, + { + "instanceSpecifier": "StateManager/LaunchManager/Instance", + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "ActivationResult" + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "queueSize": 1 + }, + { + "methodName": "GetActiveRunTarget", + "queueSize": 1 + } + ] + } + ] + } + ], + "global": { + "asil-level": "QM" + } +} diff --git a/score/launch_manager/src/lm_control/src/ilm_control.hpp b/score/launch_manager/src/lm_control/src/ilm_control.hpp index fc2f1fae7..b03cb14f1 100644 --- a/score/launch_manager/src/lm_control/src/ilm_control.hpp +++ b/score/launch_manager/src/lm_control/src/ilm_control.hpp @@ -157,7 +157,7 @@ class ILmControl /// /// @error kActivationInProgress Launch Manager is currently executing a Run Target /// activation and thus there is no single Run Target active. - /// @error kCommunicationError Connection with Launch Manager cannot be established + /// @error kCommunicationError Connection with Launch Manager cannot be established /// and information about active Run Target cannot be retrieved. virtual score::Result get_active_run_target() = 0; diff --git a/score/launch_manager/src/lm_control/tests/BUILD b/score/launch_manager/src/lm_control/tests/BUILD new file mode 100644 index 000000000..114e7fdc1 --- /dev/null +++ b/score/launch_manager/src/lm_control/tests/BUILD @@ -0,0 +1,31 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_test") + +# --------------------------------------------------------------------------- +# Component test: single-process exercise of the lm_control proxy library +# against an in-test skeleton, over the real mw::com SHM transport. +# --------------------------------------------------------------------------- + +cc_test( + name = "lm_control_CT", + srcs = ["lm_control_CT.cpp"], + args = ["$(location lm_mw_com_config.json)"], + data = ["lm_mw_com_config.json"], + deps = [ + "//score/launch_manager/src/lm_control:lm_control", + "//score/launch_manager/src/lm_control:lm_control_service", + "@googletest//:gtest", + "@score_communication//score/mw/com", + ], +) diff --git a/score/launch_manager/src/lm_control/tests/lm_control_CT.cpp b/score/launch_manager/src/lm_control/tests/lm_control_CT.cpp new file mode 100644 index 000000000..4b358c2d8 --- /dev/null +++ b/score/launch_manager/src/lm_control/tests/lm_control_CT.cpp @@ -0,0 +1,385 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +/// @file +/// @brief Single-process component test for the `lm_control` proxy library. +/// +/// The System Under Test is the `ILmControl` proxy. To exercise it end-to-end +/// without a second process, this test hosts a self-contained `FakeLaunchManager` +/// built directly on the raw `LmControlSkeleton`. Both sides communicate over the +/// real mw::com SHM transport within this process, so the proxy is used exactly +/// as a real State Manager would use it. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "score/mw/com/runtime.h" +#include "score/mw/com/runtime_configuration.h" +#include "score/mw/lifecycle/ilm_control.hpp" +#include "score/mw/lifecycle/lm_control_service.h" + +namespace score::mw::lifecycle +{ +namespace +{ + +constexpr std::string_view kProviderSpecifier = "LaunchManager/StateManager/Instance"; +constexpr std::string_view kConsumerSpecifier = "StateManager/LaunchManager/Instance"; + +/// @brief In-test replacement for the Launch Manager side. +/// +/// Offers the LmControlSkeleton and reproduces the stub behavior used by the +/// two-process integration test: `GetActiveRunTarget` reports "Running", and +/// each `ActivateRunTarget` settles asynchronously via the `activation_result` +/// event. A request whose name starts with "Fail" is treated as a failure the +/// graph recovers from, settling on "Recovery" attributed to a recovery action; +/// any other request settles on the requested Run Target attributed to a State +/// Manager request. +/// +/// Completions are dispatched from a dedicated worker thread, never from the +/// method-handler thread, to avoid deadlocking against the thread that services +/// the proxy's receive handler. +class FakeLaunchManager final +{ + public: + ~FakeLaunchManager() noexcept + { + Stop(); + } + + bool Start(std::string_view instance_specifier) + { + auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); + if (!specifier_result.has_value()) + { + std::cerr << "FakeLaunchManager: invalid instance specifier: " << instance_specifier << "\n"; + return false; + } + + auto create_result = LmControlSkeleton::Create(specifier_result.value()); + if (!create_result.has_value()) + { + std::cerr << "FakeLaunchManager: failed to create skeleton\n"; + return false; + } + skeleton_.emplace(std::move(create_result).value()); + + skeleton_->activate_run_target.RegisterHandler( + [this](ActivateRunTargetResponse& response, const ActivateRunTargetRequest& request) { + response = OnActivateRunTarget(request); + }); + + skeleton_->get_active_run_target.RegisterHandler( + [this](GetActiveRunTargetResponse& response) { + response = OnGetActiveRunTarget(); + }); + + auto offer_result = skeleton_->OfferService(); + if (!offer_result.has_value()) + { + std::cerr << "FakeLaunchManager: OfferService failed\n"; + skeleton_.reset(); + return false; + } + + worker_ = std::thread([this]() { + Run(); + }); + return true; + } + + void Stop() + { + { + std::lock_guard lock{mutex_}; + stop_ = true; + } + cv_.notify_all(); + if (worker_.joinable()) + { + worker_.join(); + } + if (skeleton_.has_value()) + { + skeleton_->StopOfferService(); + skeleton_.reset(); + } + } + + private: + struct Completion + { + RunTargetName run_target; + RunTargetActivationSource source; + }; + + GetActiveRunTargetResponse OnGetActiveRunTarget() + { + return GetActiveRunTargetResponse{QueryStatus::kAvailable, RunTargetName{"Running"}, ExecErrc::kGeneralError}; + } + + ActivateRunTargetResponse OnActivateRunTarget(const ActivateRunTargetRequest& request) + { + Completion completion{}; + if (request.run_target_name.as_string_view().rfind("Fail", 0) == 0) + { + completion.run_target = RunTargetName{"Recovery"}; + completion.source = RunTargetActivationSource::kRecoveryAction; + } + else + { + completion.run_target = request.run_target_name; + completion.source = RunTargetActivationSource::kStateManagerRequest; + } + + { + std::lock_guard lock{mutex_}; + pending_.push_back(completion); + } + cv_.notify_all(); + + return ActivateRunTargetResponse{RequestStatus::kAccepted, ExecErrc::kGeneralError}; + } + + void Run() + { + for (;;) + { + std::vector completions; + { + std::unique_lock lock{mutex_}; + cv_.wait(lock, [this]() { + return stop_ || !pending_.empty(); + }); + if (stop_ && pending_.empty()) + { + return; + } + completions.swap(pending_); + } + + for (const auto& completion : completions) + { + SendActivationResult(completion); + } + } + } + + void SendActivationResult(const Completion& completion) + { + if (!skeleton_.has_value()) + { + return; + } + auto alloc = skeleton_->activation_result.Allocate(); + if (!alloc.has_value()) + { + std::cerr << "FakeLaunchManager: Allocate() failed, activation result not sent\n"; + return; + } + alloc.value().Get()->activated_run_target = completion.run_target; + alloc.value().Get()->activation_source = completion.source; + skeleton_->activation_result.Send(std::move(alloc).value()); + } + + std::optional skeleton_; + std::thread worker_; + std::mutex mutex_; + std::condition_variable cv_; + std::vector pending_; + bool stop_{false}; +}; + +/// @brief Wait until the proxy's background discovery has connected to the +/// skeleton, detected by get_active_run_target() succeeding. +bool WaitUntilConnected(ILmControl& lm, std::chrono::milliseconds timeout) +{ + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) + { + if (lm.get_active_run_target().has_value()) + { + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + return false; +} + +} // namespace + +// The skeleton and proxy are created once for the whole suite. A single, long- +// lived proxy mirrors how a real State Manager uses the API and, crucially, +// keeps continuously draining the broadcast activation_result event. Creating a +// fresh proxy per test would instead let a newly-subscribed proxy pick up the +// previous test's still-resident sample from shared memory, firing the callback +// an extra time. +class LmControlCT : public ::testing::Test +{ + protected: + static void SetUpTestSuite() + { + fake_lm_ = new FakeLaunchManager{}; + ASSERT_TRUE(fake_lm_->Start(kProviderSpecifier)) << "FakeLaunchManager failed to start"; + + auto result = ILmControl::Create(std::string{kConsumerSpecifier}); + ASSERT_TRUE(result.has_value()) << "ILmControl::Create failed"; + lm_ = result.value().release(); + ASSERT_TRUE(WaitUntilConnected(*lm_, std::chrono::seconds(5))) + << "proxy did not connect to the skeleton in time"; + } + + static void TearDownTestSuite() + { + delete lm_; + lm_ = nullptr; + fake_lm_->Stop(); + delete fake_lm_; + fake_lm_ = nullptr; + } + + static FakeLaunchManager* fake_lm_; + static ILmControl* lm_; +}; + +FakeLaunchManager* LmControlCT::fake_lm_ = nullptr; +ILmControl* LmControlCT::lm_ = nullptr; + +TEST_F(LmControlCT, GetActiveRunTargetReturnsRunning) +{ + auto result = lm_->get_active_run_target(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "Running"); +} + +TEST_F(LmControlCT, ActivateRunTargetTriggersStateManagerRequestCallback) +{ + std::optional name{}; + std::optional source{}; + std::promise sync_promise; + auto future = sync_promise.get_future(); + + lm_->register_run_target_activation_callback( + [&sync_promise, &name, &source](RunTargetActivationSource s, RunTargetName n) { + source = s; + name = n; + sync_promise.set_value(); + }); + + auto result = lm_->activate_run_target("Running"); + ASSERT_TRUE(result.has_value()); + + ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); + EXPECT_EQ(name.value(), "Running"); + EXPECT_EQ(source.value(), RunTargetActivationSource::kStateManagerRequest); +} + +TEST_F(LmControlCT, FailingActivateRunTargetTriggersRecoveryCallback) +{ + std::optional name{}; + std::optional source{}; + std::promise sync_promise; + auto future = sync_promise.get_future(); + + lm_->register_run_target_activation_callback( + [&sync_promise, &name, &source](RunTargetActivationSource s, RunTargetName n) { + source = s; + name = n; + sync_promise.set_value(); + }); + + auto result = lm_->activate_run_target("FailRunning"); + ASSERT_TRUE(result.has_value()); + + ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); + EXPECT_EQ(name.value(), "Recovery"); + EXPECT_EQ(source.value(), RunTargetActivationSource::kRecoveryAction); +} + +TEST_F(LmControlCT, MultipleActivateRunTargetCallsYieldMultipleCallbacks) +{ + std::vector names; + std::vector sources; + std::promise sync_promise; + auto future = sync_promise.get_future(); + + lm_->register_run_target_activation_callback( + [&sync_promise, &names, &sources](RunTargetActivationSource s, RunTargetName n) { + sources.push_back(s); + names.push_back(n); + if (names.size() == 2) + { + sync_promise.set_value(); + } + }); + + auto result1 = lm_->activate_run_target("Running1"); + ASSERT_TRUE(result1.has_value()); + + auto result2 = lm_->activate_run_target("Running2"); + ASSERT_TRUE(result2.has_value()); + + ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); + ASSERT_EQ(names.size(), 2U); + EXPECT_EQ(names[0], "Running1"); + EXPECT_EQ(sources[0], RunTargetActivationSource::kStateManagerRequest); + EXPECT_EQ(names[1], "Running2"); + EXPECT_EQ(sources[1], RunTargetActivationSource::kStateManagerRequest); +} + +} // namespace score::mw::lifecycle + +namespace +{ +/// @brief Locate the mw::com manifest path among the program arguments. +/// Passed via the cc_test `args` attribute as the runfiles-relative +/// location of lm_mw_com_config.json. +const char* FindConfigPath(int argc, char** argv) +{ + for (int i = 1; i < argc; ++i) + { + const std::string_view arg{argv[i]}; + if (arg.size() >= 5U && arg.substr(arg.size() - 5U) == ".json") + { + return argv[i]; + } + } + return nullptr; +} +} // namespace + +/// Usage: lm_control_CT +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + + const char* config_path = FindConfigPath(argc, argv); + if (config_path == nullptr) + { + std::cerr << "Usage: lm_control_CT \n"; + return 1; + } + + score::mw::com::runtime::InitializeRuntime(score::mw::com::runtime::RuntimeConfiguration{config_path}); + + return RUN_ALL_TESTS(); +} diff --git a/score/launch_manager/src/lm_control/tests/lm_mw_com_config.json b/score/launch_manager/src/lm_control/tests/lm_mw_com_config.json new file mode 100644 index 000000000..a478be5d7 --- /dev/null +++ b/score/launch_manager/src/lm_control/tests/lm_mw_com_config.json @@ -0,0 +1,100 @@ +{ + "serviceTypes": [ + { + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "bindings": [ + { + "binding": "SHM", + "serviceId": 7101, + "events": [ + { + "eventName": "ActivationResult", + "eventId": 1 + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "methodId": 2 + }, + { + "methodName": "GetActiveRunTarget", + "methodId": 3 + } + ] + } + ] + } + ], + "serviceInstances": [ + { + "instanceSpecifier": "LaunchManager/StateManager/Instance", + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "ActivationResult", + "numberOfSampleSlots": 8, + "maxSubscribers": 1 + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "queueSize": 1 + }, + { + "methodName": "GetActiveRunTarget", + "queueSize": 1 + } + ] + } + ] + }, + { + "instanceSpecifier": "StateManager/LaunchManager/Instance", + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "ActivationResult" + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "queueSize": 1 + }, + { + "methodName": "GetActiveRunTarget", + "queueSize": 1 + } + ] + } + ] + } + ], + "global": { + "asil-level": "QM" + } +} From ded0df1907e751b32d003966dc1d3de47644d61b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 10:09:00 +0200 Subject: [PATCH 11/27] Drop component tests --- .../launch_manager/src/lm_control/tests/BUILD | 31 -- .../src/lm_control/tests/lm_control_CT.cpp | 385 ------------------ .../lm_control/tests/lm_mw_com_config.json | 100 ----- 3 files changed, 516 deletions(-) delete mode 100644 score/launch_manager/src/lm_control/tests/BUILD delete mode 100644 score/launch_manager/src/lm_control/tests/lm_control_CT.cpp delete mode 100644 score/launch_manager/src/lm_control/tests/lm_mw_com_config.json diff --git a/score/launch_manager/src/lm_control/tests/BUILD b/score/launch_manager/src/lm_control/tests/BUILD deleted file mode 100644 index 114e7fdc1..000000000 --- a/score/launch_manager/src/lm_control/tests/BUILD +++ /dev/null @@ -1,31 +0,0 @@ -# ******************************************************************************* -# 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 -# ******************************************************************************* -load("@rules_cc//cc:defs.bzl", "cc_test") - -# --------------------------------------------------------------------------- -# Component test: single-process exercise of the lm_control proxy library -# against an in-test skeleton, over the real mw::com SHM transport. -# --------------------------------------------------------------------------- - -cc_test( - name = "lm_control_CT", - srcs = ["lm_control_CT.cpp"], - args = ["$(location lm_mw_com_config.json)"], - data = ["lm_mw_com_config.json"], - deps = [ - "//score/launch_manager/src/lm_control:lm_control", - "//score/launch_manager/src/lm_control:lm_control_service", - "@googletest//:gtest", - "@score_communication//score/mw/com", - ], -) diff --git a/score/launch_manager/src/lm_control/tests/lm_control_CT.cpp b/score/launch_manager/src/lm_control/tests/lm_control_CT.cpp deleted file mode 100644 index 4b358c2d8..000000000 --- a/score/launch_manager/src/lm_control/tests/lm_control_CT.cpp +++ /dev/null @@ -1,385 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -/// @file -/// @brief Single-process component test for the `lm_control` proxy library. -/// -/// The System Under Test is the `ILmControl` proxy. To exercise it end-to-end -/// without a second process, this test hosts a self-contained `FakeLaunchManager` -/// built directly on the raw `LmControlSkeleton`. Both sides communicate over the -/// real mw::com SHM transport within this process, so the proxy is used exactly -/// as a real State Manager would use it. - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "score/mw/com/runtime.h" -#include "score/mw/com/runtime_configuration.h" -#include "score/mw/lifecycle/ilm_control.hpp" -#include "score/mw/lifecycle/lm_control_service.h" - -namespace score::mw::lifecycle -{ -namespace -{ - -constexpr std::string_view kProviderSpecifier = "LaunchManager/StateManager/Instance"; -constexpr std::string_view kConsumerSpecifier = "StateManager/LaunchManager/Instance"; - -/// @brief In-test replacement for the Launch Manager side. -/// -/// Offers the LmControlSkeleton and reproduces the stub behavior used by the -/// two-process integration test: `GetActiveRunTarget` reports "Running", and -/// each `ActivateRunTarget` settles asynchronously via the `activation_result` -/// event. A request whose name starts with "Fail" is treated as a failure the -/// graph recovers from, settling on "Recovery" attributed to a recovery action; -/// any other request settles on the requested Run Target attributed to a State -/// Manager request. -/// -/// Completions are dispatched from a dedicated worker thread, never from the -/// method-handler thread, to avoid deadlocking against the thread that services -/// the proxy's receive handler. -class FakeLaunchManager final -{ - public: - ~FakeLaunchManager() noexcept - { - Stop(); - } - - bool Start(std::string_view instance_specifier) - { - auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); - if (!specifier_result.has_value()) - { - std::cerr << "FakeLaunchManager: invalid instance specifier: " << instance_specifier << "\n"; - return false; - } - - auto create_result = LmControlSkeleton::Create(specifier_result.value()); - if (!create_result.has_value()) - { - std::cerr << "FakeLaunchManager: failed to create skeleton\n"; - return false; - } - skeleton_.emplace(std::move(create_result).value()); - - skeleton_->activate_run_target.RegisterHandler( - [this](ActivateRunTargetResponse& response, const ActivateRunTargetRequest& request) { - response = OnActivateRunTarget(request); - }); - - skeleton_->get_active_run_target.RegisterHandler( - [this](GetActiveRunTargetResponse& response) { - response = OnGetActiveRunTarget(); - }); - - auto offer_result = skeleton_->OfferService(); - if (!offer_result.has_value()) - { - std::cerr << "FakeLaunchManager: OfferService failed\n"; - skeleton_.reset(); - return false; - } - - worker_ = std::thread([this]() { - Run(); - }); - return true; - } - - void Stop() - { - { - std::lock_guard lock{mutex_}; - stop_ = true; - } - cv_.notify_all(); - if (worker_.joinable()) - { - worker_.join(); - } - if (skeleton_.has_value()) - { - skeleton_->StopOfferService(); - skeleton_.reset(); - } - } - - private: - struct Completion - { - RunTargetName run_target; - RunTargetActivationSource source; - }; - - GetActiveRunTargetResponse OnGetActiveRunTarget() - { - return GetActiveRunTargetResponse{QueryStatus::kAvailable, RunTargetName{"Running"}, ExecErrc::kGeneralError}; - } - - ActivateRunTargetResponse OnActivateRunTarget(const ActivateRunTargetRequest& request) - { - Completion completion{}; - if (request.run_target_name.as_string_view().rfind("Fail", 0) == 0) - { - completion.run_target = RunTargetName{"Recovery"}; - completion.source = RunTargetActivationSource::kRecoveryAction; - } - else - { - completion.run_target = request.run_target_name; - completion.source = RunTargetActivationSource::kStateManagerRequest; - } - - { - std::lock_guard lock{mutex_}; - pending_.push_back(completion); - } - cv_.notify_all(); - - return ActivateRunTargetResponse{RequestStatus::kAccepted, ExecErrc::kGeneralError}; - } - - void Run() - { - for (;;) - { - std::vector completions; - { - std::unique_lock lock{mutex_}; - cv_.wait(lock, [this]() { - return stop_ || !pending_.empty(); - }); - if (stop_ && pending_.empty()) - { - return; - } - completions.swap(pending_); - } - - for (const auto& completion : completions) - { - SendActivationResult(completion); - } - } - } - - void SendActivationResult(const Completion& completion) - { - if (!skeleton_.has_value()) - { - return; - } - auto alloc = skeleton_->activation_result.Allocate(); - if (!alloc.has_value()) - { - std::cerr << "FakeLaunchManager: Allocate() failed, activation result not sent\n"; - return; - } - alloc.value().Get()->activated_run_target = completion.run_target; - alloc.value().Get()->activation_source = completion.source; - skeleton_->activation_result.Send(std::move(alloc).value()); - } - - std::optional skeleton_; - std::thread worker_; - std::mutex mutex_; - std::condition_variable cv_; - std::vector pending_; - bool stop_{false}; -}; - -/// @brief Wait until the proxy's background discovery has connected to the -/// skeleton, detected by get_active_run_target() succeeding. -bool WaitUntilConnected(ILmControl& lm, std::chrono::milliseconds timeout) -{ - const auto deadline = std::chrono::steady_clock::now() + timeout; - while (std::chrono::steady_clock::now() < deadline) - { - if (lm.get_active_run_target().has_value()) - { - return true; - } - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - } - return false; -} - -} // namespace - -// The skeleton and proxy are created once for the whole suite. A single, long- -// lived proxy mirrors how a real State Manager uses the API and, crucially, -// keeps continuously draining the broadcast activation_result event. Creating a -// fresh proxy per test would instead let a newly-subscribed proxy pick up the -// previous test's still-resident sample from shared memory, firing the callback -// an extra time. -class LmControlCT : public ::testing::Test -{ - protected: - static void SetUpTestSuite() - { - fake_lm_ = new FakeLaunchManager{}; - ASSERT_TRUE(fake_lm_->Start(kProviderSpecifier)) << "FakeLaunchManager failed to start"; - - auto result = ILmControl::Create(std::string{kConsumerSpecifier}); - ASSERT_TRUE(result.has_value()) << "ILmControl::Create failed"; - lm_ = result.value().release(); - ASSERT_TRUE(WaitUntilConnected(*lm_, std::chrono::seconds(5))) - << "proxy did not connect to the skeleton in time"; - } - - static void TearDownTestSuite() - { - delete lm_; - lm_ = nullptr; - fake_lm_->Stop(); - delete fake_lm_; - fake_lm_ = nullptr; - } - - static FakeLaunchManager* fake_lm_; - static ILmControl* lm_; -}; - -FakeLaunchManager* LmControlCT::fake_lm_ = nullptr; -ILmControl* LmControlCT::lm_ = nullptr; - -TEST_F(LmControlCT, GetActiveRunTargetReturnsRunning) -{ - auto result = lm_->get_active_run_target(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result.value(), "Running"); -} - -TEST_F(LmControlCT, ActivateRunTargetTriggersStateManagerRequestCallback) -{ - std::optional name{}; - std::optional source{}; - std::promise sync_promise; - auto future = sync_promise.get_future(); - - lm_->register_run_target_activation_callback( - [&sync_promise, &name, &source](RunTargetActivationSource s, RunTargetName n) { - source = s; - name = n; - sync_promise.set_value(); - }); - - auto result = lm_->activate_run_target("Running"); - ASSERT_TRUE(result.has_value()); - - ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); - EXPECT_EQ(name.value(), "Running"); - EXPECT_EQ(source.value(), RunTargetActivationSource::kStateManagerRequest); -} - -TEST_F(LmControlCT, FailingActivateRunTargetTriggersRecoveryCallback) -{ - std::optional name{}; - std::optional source{}; - std::promise sync_promise; - auto future = sync_promise.get_future(); - - lm_->register_run_target_activation_callback( - [&sync_promise, &name, &source](RunTargetActivationSource s, RunTargetName n) { - source = s; - name = n; - sync_promise.set_value(); - }); - - auto result = lm_->activate_run_target("FailRunning"); - ASSERT_TRUE(result.has_value()); - - ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); - EXPECT_EQ(name.value(), "Recovery"); - EXPECT_EQ(source.value(), RunTargetActivationSource::kRecoveryAction); -} - -TEST_F(LmControlCT, MultipleActivateRunTargetCallsYieldMultipleCallbacks) -{ - std::vector names; - std::vector sources; - std::promise sync_promise; - auto future = sync_promise.get_future(); - - lm_->register_run_target_activation_callback( - [&sync_promise, &names, &sources](RunTargetActivationSource s, RunTargetName n) { - sources.push_back(s); - names.push_back(n); - if (names.size() == 2) - { - sync_promise.set_value(); - } - }); - - auto result1 = lm_->activate_run_target("Running1"); - ASSERT_TRUE(result1.has_value()); - - auto result2 = lm_->activate_run_target("Running2"); - ASSERT_TRUE(result2.has_value()); - - ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); - ASSERT_EQ(names.size(), 2U); - EXPECT_EQ(names[0], "Running1"); - EXPECT_EQ(sources[0], RunTargetActivationSource::kStateManagerRequest); - EXPECT_EQ(names[1], "Running2"); - EXPECT_EQ(sources[1], RunTargetActivationSource::kStateManagerRequest); -} - -} // namespace score::mw::lifecycle - -namespace -{ -/// @brief Locate the mw::com manifest path among the program arguments. -/// Passed via the cc_test `args` attribute as the runfiles-relative -/// location of lm_mw_com_config.json. -const char* FindConfigPath(int argc, char** argv) -{ - for (int i = 1; i < argc; ++i) - { - const std::string_view arg{argv[i]}; - if (arg.size() >= 5U && arg.substr(arg.size() - 5U) == ".json") - { - return argv[i]; - } - } - return nullptr; -} -} // namespace - -/// Usage: lm_control_CT -int main(int argc, char** argv) -{ - ::testing::InitGoogleTest(&argc, argv); - - const char* config_path = FindConfigPath(argc, argv); - if (config_path == nullptr) - { - std::cerr << "Usage: lm_control_CT \n"; - return 1; - } - - score::mw::com::runtime::InitializeRuntime(score::mw::com::runtime::RuntimeConfiguration{config_path}); - - return RUN_ALL_TESTS(); -} diff --git a/score/launch_manager/src/lm_control/tests/lm_mw_com_config.json b/score/launch_manager/src/lm_control/tests/lm_mw_com_config.json deleted file mode 100644 index a478be5d7..000000000 --- a/score/launch_manager/src/lm_control/tests/lm_mw_com_config.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "serviceTypes": [ - { - "serviceTypeName": "/score/mw/lifecycle/LmControlService", - "version": { - "major": 1, - "minor": 0 - }, - "bindings": [ - { - "binding": "SHM", - "serviceId": 7101, - "events": [ - { - "eventName": "ActivationResult", - "eventId": 1 - } - ], - "methods": [ - { - "methodName": "ActivateRunTarget", - "methodId": 2 - }, - { - "methodName": "GetActiveRunTarget", - "methodId": 3 - } - ] - } - ] - } - ], - "serviceInstances": [ - { - "instanceSpecifier": "LaunchManager/StateManager/Instance", - "serviceTypeName": "/score/mw/lifecycle/LmControlService", - "version": { - "major": 1, - "minor": 0 - }, - "instances": [ - { - "instanceId": 1, - "asil-level": "QM", - "binding": "SHM", - "events": [ - { - "eventName": "ActivationResult", - "numberOfSampleSlots": 8, - "maxSubscribers": 1 - } - ], - "methods": [ - { - "methodName": "ActivateRunTarget", - "queueSize": 1 - }, - { - "methodName": "GetActiveRunTarget", - "queueSize": 1 - } - ] - } - ] - }, - { - "instanceSpecifier": "StateManager/LaunchManager/Instance", - "serviceTypeName": "/score/mw/lifecycle/LmControlService", - "version": { - "major": 1, - "minor": 0 - }, - "instances": [ - { - "instanceId": 1, - "asil-level": "QM", - "binding": "SHM", - "events": [ - { - "eventName": "ActivationResult" - } - ], - "methods": [ - { - "methodName": "ActivateRunTarget", - "queueSize": 1 - }, - { - "methodName": "GetActiveRunTarget", - "queueSize": 1 - } - ] - } - ] - } - ], - "global": { - "asil-level": "QM" - } -} From 636ba66a1aa9d99876661d4dfe8e32ca6e07118b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 10:09:21 +0200 Subject: [PATCH 12/27] Drop integration tests --- tests/integration/lm_control/BUILD | 51 ------- .../lm_control/lm_control_ipc_test.cpp | 144 ------------------ .../lm_control/lm_control_ipc_test.py | 78 ---------- .../lm_control/lm_mw_com_config.json | 76 --------- .../lm_control/lm_skeleton_stub.cpp | 137 ----------------- .../lm_control/sm_mw_com_config.json | 74 --------- 6 files changed, 560 deletions(-) delete mode 100644 tests/integration/lm_control/BUILD delete mode 100644 tests/integration/lm_control/lm_control_ipc_test.cpp delete mode 100644 tests/integration/lm_control/lm_control_ipc_test.py delete mode 100644 tests/integration/lm_control/lm_mw_com_config.json delete mode 100644 tests/integration/lm_control/lm_skeleton_stub.cpp delete mode 100644 tests/integration/lm_control/sm_mw_com_config.json diff --git a/tests/integration/lm_control/BUILD b/tests/integration/lm_control/BUILD deleted file mode 100644 index dc43892e6..000000000 --- a/tests/integration/lm_control/BUILD +++ /dev/null @@ -1,51 +0,0 @@ -# ******************************************************************************* -# 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 -# ******************************************************************************* -load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_test") -load("//tests/utils/bazel:integration.bzl", "integration_test") - -# --------------------------------------------------------------------------- -# IPC integration test (real mw::com SHM transport between two processes) -# --------------------------------------------------------------------------- - -cc_binary( - name = "lm_skeleton_stub", - srcs = ["lm_skeleton_stub.cpp"], - deps = [ - "//score/launch_manager/src/daemon/src/control:lm_control_server", - "@score_communication//score/mw/com", - ], -) - -cc_binary( - name = "lm_control_ipc_test", - srcs = ["lm_control_ipc_test.cpp"], - deps = [ - "//score/launch_manager/src/lm_control:lm_control", - "//tests/utils/test_helper", - "@googletest//:gtest", - "@score_communication//score/mw/com", - ], -) - -integration_test( - name = "lm_control_ipc", - srcs = ["lm_control_ipc_test.py"], - binaries = [ - ":lm_skeleton_stub", - ":lm_control_ipc_test", - ], - files = [ - "lm_mw_com_config.json", - "sm_mw_com_config.json", - ], -) diff --git a/tests/integration/lm_control/lm_control_ipc_test.cpp b/tests/integration/lm_control/lm_control_ipc_test.cpp deleted file mode 100644 index c09f9634d..000000000 --- a/tests/integration/lm_control/lm_control_ipc_test.cpp +++ /dev/null @@ -1,144 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ -#include - -#include -#include -#include - -#include "score/mw/com/runtime.h" -#include "score/mw/com/runtime_configuration.h" -#include "score/mw/lifecycle/ilm_control.hpp" -#include "tests/utils/test_helper/test_helper.hpp" - -namespace score::mw::lifecycle -{ - -class LmControlIPCTest : public ::testing::Test -{ - protected: - void SetUp() override - { - auto result = ILmControl::Create("StateManager/LaunchManager/Instance"); - ASSERT_TRUE(result.has_value()) << "ILmControl::Create failed"; - lm_ = std::move(result).value(); - } - - std::unique_ptr lm_; -}; - -TEST_F(LmControlIPCTest, LmControlAPI) -{ - TEST_STEP("GetActiveRunTarget returns Running") - { - auto result = lm_->get_active_run_target(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result.value(), "Running"); - } - - TEST_STEP("ActivateRunTarget is successful and triggers ActivationCallback with StateManagerRequest") - { - std::optional name{}; - std::optional source{}; - std::promise sync_promise; - auto future = sync_promise.get_future(); - - lm_->register_run_target_activation_callback( - [&sync_promise, &name, &source](RunTargetActivationSource s, RunTargetName n) { - source = s; - name = n; - sync_promise.set_value(); - }); - - auto result = lm_->activate_run_target("Running"); - ASSERT_TRUE(result.has_value()); - - ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); - EXPECT_EQ(name.value(), "Running"); - EXPECT_EQ(source.value(), RunTargetActivationSource::kStateManagerRequest); - } - - TEST_STEP("ActivateRunTarget fails and activates recovery action. Triggers ActivationCallback with kRecoveryAction") - { - std::optional name{}; - std::optional source{}; - std::promise sync_promise; - auto future = sync_promise.get_future(); - - lm_->register_run_target_activation_callback( - [&sync_promise, &name, &source](RunTargetActivationSource s, RunTargetName n) { - source = s; - name = n; - sync_promise.set_value(); - }); - - auto result = lm_->activate_run_target("FailRunning"); - ASSERT_TRUE(result.has_value()); - - ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); - EXPECT_EQ(name.value(), "Recovery"); - EXPECT_EQ(source.value(), RunTargetActivationSource::kRecoveryAction); - } - - TEST_STEP("Can call ActivateRunTarget multiple times and get multiple callbacks") - { - std::vector names; - std::vector sources; - std::promise sync_promise; - auto future = sync_promise.get_future(); - - lm_->register_run_target_activation_callback( - [&sync_promise, &names, &sources](RunTargetActivationSource s, RunTargetName n) { - sources.push_back(s); - names.push_back(n); - if (names.size() == 2) - { - sync_promise.set_value(); - } - }); - - auto result1 = lm_->activate_run_target("Running1"); - ASSERT_TRUE(result1.has_value()); - - auto result2 = lm_->activate_run_target("Running2"); - ASSERT_TRUE(result2.has_value()); - - ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), std::future_status::ready); - EXPECT_EQ(names[0], "Running1"); - EXPECT_EQ(sources[0], RunTargetActivationSource::kStateManagerRequest); - EXPECT_EQ(names[1], "Running2"); - EXPECT_EQ(sources[1], RunTargetActivationSource::kStateManagerRequest); - } -} - -} // namespace score::mw::lifecycle - -/// Usage: lm_control_ipc_test -int main(int argc, const char* argv[]) -{ - if (argc < 2) - { - std::cerr << "Usage: lm_control_ipc_test \n"; - return 1; - } - - score::mw::com::runtime::InitializeRuntime(score::mw::com::runtime::RuntimeConfiguration{argv[1]}); - - int ret; - { - ret = TestRunner(__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd).RunTests(); - } - - sleep(1); - return ret; -} diff --git a/tests/integration/lm_control/lm_control_ipc_test.py b/tests/integration/lm_control/lm_control_ipc_test.py deleted file mode 100644 index 118921fa4..000000000 --- a/tests/integration/lm_control/lm_control_ipc_test.py +++ /dev/null @@ -1,78 +0,0 @@ -# ******************************************************************************* -# 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 -# ******************************************************************************* -import logging -import time - -from attribute_plugin import add_test_properties -from tests.utils.testing_utils.run_until_file_deployed import run_until_file_deployed -from tests.utils.testing_utils.test_results import assert_test_results -from tests.utils.testing_utils.setup_test import setup_test - - -logger = logging.getLogger(__name__) - - -@add_test_properties( - partially_verifies=[], - test_type="interface-test", - derivation_technique="explorative-testing", -) -def test_lm_control_ipc(target, setup_test, assert_test_results, remote_test_dir): - """ - Objective: Verifies end-to-end IPC communication between the LmControlSkeleton - (Launch Manager side) and the ILmControl proxy (State Manager side) over mw::com SHM. - - The lm_skeleton_stub starts the LmControlServer with stub handlers. - The lm_control_ipc_test binary connects via ILmControl, exercises activate_run_target - and get_active_run_target, and verifies that callbacks fire correctly over IPC. - - Expected Behaviour: All ILmControl API calls succeed and the activation callback - is invoked with the correct RunTargetName and RunTargetActivationSource. - """ - test_dir = remote_test_dir - - lm_config = str(test_dir / "lm_mw_com_config.json") - sm_config = str(test_dir / "sm_mw_com_config.json") - lm_stub = str(test_dir / "lm_skeleton_stub") - sm_client = str(test_dir / "lm_control_ipc_test") - - # Start the LM skeleton stub — runs in the background. - logger.info("Starting lm_skeleton_stub") - lm_proc = target.execute_async(lm_stub, args=["--service_instance_manifest", lm_config], cwd=str(test_dir)) - - # Wait for the skeleton to offer the service. - # ILmControl::Create polls for up to 2 s, so 3 s gives ample margin. - time.sleep(3) - - if not lm_proc.is_running(): - raise RuntimeError( - f"lm_skeleton_stub exited early (code {lm_proc.get_exit_code()})" - ) - - # Run the SM client. It signals completion by creating the test_end file - # (TerminationNotification::kTestEnd), then waits in pause() for SIGTERM. - # run_until_file_deployed detects the file, sends SIGTERM, and the binary exits. - logger.info("Running lm_control_ipc_test") - run_until_file_deployed( - target=target, - binary_path=sm_client, - file_path=remote_test_dir.parent / "test_end", - cwd=str(remote_test_dir), - args=[sm_config], - timeout_s=30.0, - ) - - # Shut down the LM skeleton. - lm_proc.stop() - - assert_test_results({"lm_control_ipc_test.xml"}) diff --git a/tests/integration/lm_control/lm_mw_com_config.json b/tests/integration/lm_control/lm_mw_com_config.json deleted file mode 100644 index f309c5481..000000000 --- a/tests/integration/lm_control/lm_mw_com_config.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "serviceTypes": [ - { - "serviceTypeName": "/score/mw/lifecycle/LmControlService", - "version": { - "major": 1, - "minor": 0 - }, - "bindings": [ - { - "binding": "SHM", - "serviceId": 7001, - "events": [ - { - "eventName": "ActivationResult", - "eventId": 1 - } - ], - "methods": [ - { - "methodName": "ActivateRunTarget", - "methodId": 2 - }, - { - "methodName": "GetActiveRunTarget", - "methodId": 3 - } - ] - } - ] - } - ], - "serviceInstances": [ - { - "instanceSpecifier": "LaunchManager/StateManager/Instance", - "serviceTypeName": "/score/mw/lifecycle/LmControlService", - "version": { - "major": 1, - "minor": 0 - }, - "instances": [ - { - "instanceId": 1, - "asil-level": "QM", - "binding": "SHM", - "events": [ - { - "eventName": "ActivationResult", - "numberOfSampleSlots": 8, - "maxSubscribers": 1 - } - ], - "methods": [ - { - "methodName": "ActivateRunTarget", - "queueSize": 1 - }, - { - "methodName": "GetActiveRunTarget", - "queueSize": 1 - } - ], - "allowedConsumer": { - "QM": [0] - }, - "allowedProvider": { - "QM": [0] - } - } - ] - } - ], - "global": { - "asil-level": "QM" - } -} diff --git a/tests/integration/lm_control/lm_skeleton_stub.cpp b/tests/integration/lm_control/lm_skeleton_stub.cpp deleted file mode 100644 index 9842517f0..000000000 --- a/tests/integration/lm_control/lm_skeleton_stub.cpp +++ /dev/null @@ -1,137 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include - -#include "score/mw/com/runtime.h" -#include "score/mw/com/runtime_configuration.h" -#include "score/mw/launch_manager/control/lm_control_server.hpp" - -namespace -{ -using score::mw::launch_manager::control::IGraph; -using score::mw::launch_manager::control::LmControlServer; -using score::mw::launch_manager::control::RunTargetRequest; - -std::atomic g_stop{false}; - -void on_signal(int) noexcept -{ - g_stop = true; -} - -/// @brief Minimal IGraph that echoes the requested Run Target back as -/// settled, once processRequests() is called from the main loop. -class StubGraph final : public IGraph -{ - public: - void setServer(LmControlServer* server) noexcept - { - server_ = server; - } - - std::optional getActiveRunTarget() override - { - return "Running"; - } - - bool enqueueRunTargetActivation(RunTargetRequest request) override - { - std::lock_guard lock(mutex_); - pending_.push_back(request); - return true; - } - - /// @brief Settle all queued activations. Must be called from the main loop. - /// - /// Simulates a recovery action: activating a Run Target whose name starts - /// with "Fail" is treated as a failure that the graph recovers from by - /// settling on "Recovery" instead, reported with no requestId so - /// activationCompleted attributes it to RunTargetActivationSource::kRecoveryAction. - void processRequests() - { - std::vector requests; - { - std::lock_guard lock(mutex_); - requests.swap(pending_); - } - - for (const auto& request : requests) - { - if (server_ == nullptr) - { - continue; - } - - if (request.runTargetName.as_string_view().rfind("Fail", 0) == 0) - { - server_->activationCompleted("Recovery", std::nullopt); - } - else - { - server_->activationCompleted(request.runTargetName.as_string_view(), request.requestId); - } - } - } - - private: - LmControlServer* server_{nullptr}; - std::mutex mutex_; - std::vector pending_; -}; -} // namespace - -/// @brief Minimal binary that starts the LmControlSkeleton (stub handlers) and -/// waits for SIGTERM. Used as the Launch Manager side in IPC integration tests. -/// -/// Usage: lm_skeleton_stub -int main(int argc, const char* argv[]) -{ - if (argc < 2) - { - std::cerr << "Usage: lm_skeleton_stub --service_instance_manifest \n"; - return 1; - } - - std::signal(SIGTERM, on_signal); - std::signal(SIGINT, on_signal); - - score::mw::com::runtime::InitializeRuntime(argc, argv); - - StubGraph graph; - LmControlServer server{graph}; - graph.setServer(&server); - - if (!server.Initialize()) - { - std::cerr << "lm_skeleton_stub: LmControlServer::Initialize failed\n"; - return 1; - } - - std::cout << "lm_skeleton_stub: skeleton ready" << std::endl; - - while (!g_stop) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - graph.processRequests(); - } - - server.Shutdown(); - return 0; -} diff --git a/tests/integration/lm_control/sm_mw_com_config.json b/tests/integration/lm_control/sm_mw_com_config.json deleted file mode 100644 index 1748fdbf8..000000000 --- a/tests/integration/lm_control/sm_mw_com_config.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "serviceTypes": [ - { - "serviceTypeName": "/score/mw/lifecycle/LmControlService", - "version": { - "major": 1, - "minor": 0 - }, - "bindings": [ - { - "binding": "SHM", - "serviceId": 7001, - "events": [ - { - "eventName": "ActivationResult", - "eventId": 1 - } - ], - "methods": [ - { - "methodName": "ActivateRunTarget", - "methodId": 2 - }, - { - "methodName": "GetActiveRunTarget", - "methodId": 3 - } - ] - } - ] - } - ], - "serviceInstances": [ - { - "instanceSpecifier": "StateManager/LaunchManager/Instance", - "serviceTypeName": "/score/mw/lifecycle/LmControlService", - "version": { - "major": 1, - "minor": 0 - }, - "instances": [ - { - "instanceId": 1, - "asil-level": "QM", - "binding": "SHM", - "events": [ - { - "eventName": "ActivationResult" - } - ], - "methods": [ - { - "methodName": "ActivateRunTarget", - "queueSize": 1 - }, - { - "methodName": "GetActiveRunTarget", - "queueSize": 1 - } - ], - "allowedConsumer": { - "QM": [0] - }, - "allowedProvider": { - "QM": [0] - } - } - ] - } - ], - "global": { - "asil-level": "QM" - } -} From 4cb09064c00a0424263358c38b80752bf720782a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 10:22:22 +0200 Subject: [PATCH 13/27] Remove connected flag --- .../src/details/lm_control_impl.hpp | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index b9803251f..9f3a166ac 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -13,7 +13,6 @@ #ifndef SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP #define SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP -#include #include #include #include @@ -168,7 +167,8 @@ class BasicLmControlImpl final : public ILmControl score::Result activate_run_target(RunTargetName runTargetName, bool force = false) override { - if (!connected_.load(std::memory_order_acquire)) + auto* const proxy = connectedProxy(); + if (proxy == nullptr) { return score::MakeUnexpected(ExecErrc::kCommunicationError); } @@ -178,7 +178,7 @@ class BasicLmControlImpl final : public ILmControl const ActivateRunTargetRequest request{ runTargetName, force ? ActivationMode::kForced : ActivationMode::kQueued}; - auto result = proxy_->activate_run_target(request); + auto result = proxy->activate_run_target(request); if (!result.has_value()) { LM_LOG_ERROR() << "LmControl: activate_run_target: proxy method call failed with error:" << result.error(); @@ -208,12 +208,13 @@ class BasicLmControlImpl final : public ILmControl score::Result get_active_run_target() override { - if (!connected_.load(std::memory_order_acquire)) + auto* const proxy = connectedProxy(); + if (proxy == nullptr) { return score::MakeUnexpected(ExecErrc::kCommunicationError); } - auto result = proxy_->get_active_run_target(); + auto result = proxy->get_active_run_target(); if (!result.has_value()) { LM_LOG_ERROR() << "LmControl: get_active_run_target: proxy method call failed with error:" @@ -240,6 +241,13 @@ class BasicLmControlImpl final : public ILmControl /// @brief Allow to read small number of samples at once static constexpr std::size_t kActivationResultSampleCount = 4U; + /// @brief Returns the proxy instance or nullptr if not connected + ProxyType* connectedProxy() noexcept + { + std::lock_guard lock{mutex_}; + return proxy_.has_value() ? &proxy_.value() : nullptr; + } + /// @brief Invoked by mw::com whenever service availability changes. On the /// first matching instance it creates the proxy, subscribes to /// activation results, and stops the ongoing discovery. @@ -267,9 +275,6 @@ class BasicLmControlImpl final : public ILmControl // First matching instance is wired up — stop the ongoing discovery. stopDiscovery(find_handle); - - // Publish the fully set-up proxy_ to method-caller threads. - connected_.store(true, std::memory_order_release); } /// @brief Create the proxy for the discovered instance and publish it into proxy_. @@ -403,16 +408,12 @@ class BasicLmControlImpl final : public ILmControl // Error captured during construction (nullopt on success). std::optional init_error_; - // Guards proxy_ setup and the service discovery handles. + // Guards proxy_ and the service discovery handles. std::mutex mutex_; std::optional proxy_; std::optional find_handle_; bool find_service_stopped_{false}; - // Publishes the fully set-up proxy_ to method-caller threads. Read with - // acquire ordering in the API methods, set with release once proxy_ is ready. - std::atomic connected_{false}; - ActivationCallback callback_; std::mutex callback_mutex_; From 9cdc6892ac80fc4e8531d2b012c440ac08fcda38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 10:30:28 +0200 Subject: [PATCH 14/27] Service discovery --- .../src/details/lm_control_impl.hpp | 41 ++++++++++--------- .../src/details/lm_control_impl_UT.cpp | 4 +- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index 9f3a166ac..3dde6020d 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -129,9 +129,9 @@ class BasicLmControlImpl final : public ILmControl } std::lock_guard lock{mutex_}; - // The handler may already have run (and stopped discovery) synchronously - // during StartFindService; only remember the handle if it is still active. - if (!find_service_stopped_) + // The handler may already have run synchronously during StartFindService. It stops the + // search as soon as it has a proxy, so a set proxy_ means the handle is already spent. + if (!proxy_.has_value()) { find_handle_ = start_result.value(); } @@ -139,23 +139,17 @@ class BasicLmControlImpl final : public ILmControl ~BasicLmControlImpl() noexcept override { + // Claim the handle under the lock, but issue the stop outside it: called from outside a + // FindServiceHandler, StopFindService blocks until in-flight handlers have returned, and + // those handlers need mutex_ themselves — stopping under the lock would deadlock. std::optional handle_to_stop; { std::lock_guard lock{mutex_}; - if (!find_service_stopped_ && find_handle_.has_value()) - { - handle_to_stop = find_handle_; - find_service_stopped_ = true; - } + handle_to_stop = std::exchange(find_handle_, std::nullopt); } if (handle_to_stop.has_value()) { - auto stop_result = Traits::StopFindService(handle_to_stop.value()); - if (!stop_result.has_value()) - { - LM_LOG_ERROR() << "LmControl: StopFindService failed during shutdown with error:" - << stop_result.error(); - } + stopFindService(handle_to_stop.value()); } if (proxy_.has_value()) @@ -313,15 +307,22 @@ class BasicLmControlImpl final : public ILmControl } /// @brief Stop the ongoing service discovery now that an instance is wired up. - /// @pre Caller holds mutex_. + /// @pre Caller holds mutex_ void stopDiscovery(const FindServiceHandle& find_handle) noexcept { - auto stop_result = Traits::StopFindService(find_handle); + // Hand over the handle before stopping: the destructor stops whatever is left in find_handle_. + find_handle_.reset(); + stopFindService(find_handle); + } + + /// @brief Issue StopFindService and log a failure. Holds no state of its own. + static void stopFindService(const FindServiceHandle& find_handle) noexcept + { + const auto stop_result = Traits::StopFindService(find_handle); if (!stop_result.has_value()) { LM_LOG_ERROR() << "LmControl: StopFindService failed with error:" << stop_result.error(); } - find_service_stopped_ = true; } /// @brief mw::com receive handler for the activation_result event. @@ -408,11 +409,13 @@ class BasicLmControlImpl final : public ILmControl // Error captured during construction (nullopt on success). std::optional init_error_; - // Guards proxy_ and the service discovery handles. + // Guards proxy_ and the service discovery handle. std::mutex mutex_; std::optional proxy_; + + // Non-empty exactly while a discovery search is running that we are responsible for stopping. + // Whoever stops the search clears it, so it can never be stopped twice. std::optional find_handle_; - bool find_service_stopped_{false}; ActivationCallback callback_; std::mutex callback_mutex_; diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp index 8ec2efa1e..46085adb3 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp @@ -361,8 +361,8 @@ TEST_F(LmControlUT, SynchronousDiscoveryDuringConstructionStopsFindingOnce) handler(score::mw::com::ServiceHandleContainer{FakeHandle{}}, FakeFindHandle{}); return score::Result{FakeFindHandle{}}; })); - // Discovery is stopped exactly once (during construction), never again in the - // destructor, because construction observed find_service_stopped_ == true. + // Discovery is stopped exactly once (during construction), never again in the destructor, + // because construction observed a set proxy_ and therefore never stored the handle. EXPECT_CALL(mock_, StopFindService()).Times(1); auto sut = MakeLmControl(); From 74146862a6147d8444a122bba34ac863263ff2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 10:36:30 +0200 Subject: [PATCH 15/27] Do not stop service discovery --- .../src/details/lm_control_impl.hpp | 74 +++++-------------- .../src/details/lm_control_impl_UT.cpp | 41 +++++++--- 2 files changed, 47 insertions(+), 68 deletions(-) diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index 3dde6020d..75b918d78 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -116,8 +116,8 @@ class BasicLmControlImpl final : public ILmControl } auto start_result = Traits::StartFindService( - [this](score::mw::com::ServiceHandleContainer handles, FindServiceHandle find_handle) noexcept { - onServiceFound(std::move(handles), find_handle); + [this](score::mw::com::ServiceHandleContainer handles, FindServiceHandle) noexcept { + onServiceFound(std::move(handles)); }, std::move(specifier_result).value()); @@ -128,28 +128,20 @@ class BasicLmControlImpl final : public ILmControl return; } - std::lock_guard lock{mutex_}; - // The handler may already have run synchronously during StartFindService. It stops the - // search as soon as it has a proxy, so a set proxy_ means the handle is already spent. - if (!proxy_.has_value()) - { - find_handle_ = start_result.value(); - } + find_handle_ = start_result.value(); } ~BasicLmControlImpl() noexcept override { - // Claim the handle under the lock, but issue the stop outside it: called from outside a - // FindServiceHandler, StopFindService blocks until in-flight handlers have returned, and - // those handlers need mutex_ themselves — stopping under the lock would deadlock. - std::optional handle_to_stop; + // Stop discovery first: once StopFindService returns, onServiceFound() can no longer run, + // so the remaining teardown races nothing. + if (find_handle_.has_value()) { - std::lock_guard lock{mutex_}; - handle_to_stop = std::exchange(find_handle_, std::nullopt); - } - if (handle_to_stop.has_value()) - { - stopFindService(handle_to_stop.value()); + const auto stop_result = Traits::StopFindService(find_handle_.value()); + if (!stop_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: StopFindService failed with error:" << stop_result.error(); + } } if (proxy_.has_value()) @@ -242,12 +234,9 @@ class BasicLmControlImpl final : public ILmControl return proxy_.has_value() ? &proxy_.value() : nullptr; } - /// @brief Invoked by mw::com whenever service availability changes. On the - /// first matching instance it creates the proxy, subscribes to - /// activation results, and stops the ongoing discovery. - void onServiceFound( - score::mw::com::ServiceHandleContainer handles, - FindServiceHandle find_handle) noexcept + /// @brief Invoked by mw::com whenever service availability changes. On the first matching + /// instance it creates the proxy and subscribes to activation results. + void onServiceFound(score::mw::com::ServiceHandleContainer handles) noexcept { std::lock_guard lock{mutex_}; @@ -257,18 +246,10 @@ class BasicLmControlImpl final : public ILmControl return; } - if (!createProxy(handles.front())) + if (createProxy(handles.front())) { - return; + static_cast(subscribeToActivationResults()); } - - if (!subscribeToActivationResults()) - { - return; - } - - // First matching instance is wired up — stop the ongoing discovery. - stopDiscovery(find_handle); } /// @brief Create the proxy for the discovered instance and publish it into proxy_. @@ -306,25 +287,6 @@ class BasicLmControlImpl final : public ILmControl return true; } - /// @brief Stop the ongoing service discovery now that an instance is wired up. - /// @pre Caller holds mutex_ - void stopDiscovery(const FindServiceHandle& find_handle) noexcept - { - // Hand over the handle before stopping: the destructor stops whatever is left in find_handle_. - find_handle_.reset(); - stopFindService(find_handle); - } - - /// @brief Issue StopFindService and log a failure. Holds no state of its own. - static void stopFindService(const FindServiceHandle& find_handle) noexcept - { - const auto stop_result = Traits::StopFindService(find_handle); - if (!stop_result.has_value()) - { - LM_LOG_ERROR() << "LmControl: StopFindService failed with error:" << stop_result.error(); - } - } - /// @brief mw::com receive handler for the activation_result event. /// @details mw::com may invoke this concurrently with itself. Since proxy-event /// API calls must not run concurrently on the same event, the handler @@ -413,8 +375,8 @@ class BasicLmControlImpl final : public ILmControl std::mutex mutex_; std::optional proxy_; - // Non-empty exactly while a discovery search is running that we are responsible for stopping. - // Whoever stops the search clears it, so it can never be stopped twice. + // Handle of the running discovery search, stopped by the destructor. Written once by the + // constructor and never touched again, so it needs no lock. std::optional find_handle_; ActivationCallback callback_; diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp index 46085adb3..7d43343e6 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp @@ -308,22 +308,39 @@ TEST_F(LmControlUT, MethodsFailWhileNotYetConnected) EXPECT_EQ(get.error(), ExecErrc::kCommunicationError); } -TEST_F(LmControlUT, DiscoverySubscribesSetsHandlerAndStopsFinding) +TEST_F(LmControlUT, DiscoveryCreatesProxySubscribesAndSetsHandler) { RecordProperty( "Description", - "On discovering the service, the SUT creates the proxy, subscribes to activation results, sets the " - "receive handler, and stops discovery."); + "On discovering the service, the SUT creates the proxy, subscribes to activation results and sets the " + "receive handler."); EXPECT_CALL(mock_, CreateProxy()); EXPECT_CALL(mock_, Subscribe(4U)); // must match kActivationResultSampleCount EXPECT_CALL(mock_, SetReceiveHandler(_)); - EXPECT_CALL(mock_, StopFindService()); auto sut = MakeConnected(); EXPECT_TRUE(receive_handler_); } +TEST_F(LmControlUT, DiscoveryKeepsRunningAfterConnect) +{ + RecordProperty( + "Description", + "Connecting does not stop the discovery search; it keeps running until the instance is destroyed."); + + EXPECT_CALL(mock_, StopFindService()).Times(0); + + auto sut = MakeConnected(); + // A further notification while already connected must not stop discovery either. + ConnectService(); + EXPECT_TRUE(sut->get_active_run_target().has_value()); + + // Verify while the instance is still alive - the destructor is what stops discovery, and it + // is covered by DestructorStopsDiscoveryWhenNeverConnected / DestructorUnsubscribesWhenConnected. + ::testing::Mock::VerifyAndClearExpectations(&mock_); +} + TEST_F(LmControlUT, EmptyHandlesAreIgnored) { RecordProperty( @@ -361,8 +378,8 @@ TEST_F(LmControlUT, SynchronousDiscoveryDuringConstructionStopsFindingOnce) handler(score::mw::com::ServiceHandleContainer{FakeHandle{}}, FakeFindHandle{}); return score::Result{FakeFindHandle{}}; })); - // Discovery is stopped exactly once (during construction), never again in the destructor, - // because construction observed a set proxy_ and therefore never stored the handle. + // Discovery is stopped exactly once, by the destructor. The synchronous handler leaves the + // search untouched, so the constructor can store the handle unconditionally. EXPECT_CALL(mock_, StopFindService()).Times(1); auto sut = MakeLmControl(); @@ -400,18 +417,18 @@ TEST_F(LmControlUT, ProxyCreateFailureLeavesInstanceDisconnected) EXPECT_FALSE(sut->get_active_run_target().has_value()); } -TEST_F(LmControlUT, StopFindServiceFailureOnConnectIsTolerated) +TEST_F(LmControlUT, StopFindServiceFailureOnDestructionIsTolerated) { RecordProperty( "Description", - "A StopFindService failure on connect is only logged; the instance still becomes fully connected."); + "A StopFindService failure while tearing down is only logged; destruction still completes cleanly."); EXPECT_CALL(mock_, StopFindService()).WillOnce(Return(score::MakeUnexpected(ExecErrc::kInvalidArguments))); - auto sut = MakeConnected(); - - // The failure is only logged; the instance is fully connected. - EXPECT_TRUE(sut->get_active_run_target().has_value()); + { + auto sut = MakeConnected(); + EXPECT_TRUE(sut->get_active_run_target().has_value()); + } // The failure inside the destructor is only logged. } // --------------------------------------------------------------------------- From d5c0e357a3cb168f3f0abbcfc5e2aa5958e36a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 10:58:00 +0200 Subject: [PATCH 16/27] init refactoring --- .../lm_control/src/details/ilm_control.cpp | 9 +- .../src/details/lm_control_impl.hpp | 75 ++++++------- .../src/details/lm_control_impl_UT.cpp | 100 ++++++++++++++---- 3 files changed, 119 insertions(+), 65 deletions(-) diff --git a/score/launch_manager/src/lm_control/src/details/ilm_control.cpp b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp index 5d08821f2..a17e777f3 100644 --- a/score/launch_manager/src/lm_control/src/details/ilm_control.cpp +++ b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp @@ -19,12 +19,13 @@ namespace score::mw::lifecycle score::Result> ILmControl::Create(std::string_view instance_specifier) { - auto impl_result = LmControlImpl::Create(instance_specifier); - if (!impl_result.has_value()) + auto instance = std::make_unique(); + const auto init_result = instance->init(instance_specifier); + if (!init_result.has_value()) { - return score::MakeUnexpected>(impl_result.error()); + return score::MakeUnexpected>(init_result.error()); } - return std::move(impl_result).value(); + return instance; } } // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index 75b918d78..e9254c1bd 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -14,7 +14,6 @@ #define SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP #include -#include #include #include #include @@ -70,12 +69,11 @@ struct MwComProxyTraits /// @brief mw::com proxy-based implementation of ILmControl. /// -/// Connects to the Launch Manager via mw::com. -/// Construction starts an asynchronous service discovery that -/// keeps searching for the instance in the background. Construction itself -/// succeeds as long as the instance specifier is valid and there is no error -// setting up the service discovery; the proxy may only become available later. -// Until it does, method calls return kCommunicationError. +/// Connects to the Launch Manager via mw::com. Construction yields an inert, unconnected +/// object; init() then starts an asynchronous service discovery that keeps searching for +/// the instance in the background. init() succeeds as long as the instance specifier is +/// valid and the service discovery could be started; the proxy may only become available +/// later. Until it does, method calls return kCommunicationError. /// /// @tparam Traits Binds the class to a proxy implementation. Defaults to /// MwComProxyTraits (the real mw::com proxy). Tests inject a @@ -85,34 +83,25 @@ template class BasicLmControlImpl final : public ILmControl { public: - /// @brief Create a fully initialized instance, or return the construction error. - /// @param[in] instance_specifier The mw::com instance specifier identifying - /// the Launch Manager service. Must be a - /// non-empty, valid specifier string. An empty - /// or malformed value yields kInvalidArguments. - /// @return The ready-to-use instance, or the error that prevented setup. - static score::Result> Create(std::string_view instance_specifier) noexcept - { - auto instance = std::make_unique(instance_specifier); - if (instance->init_error_.has_value()) - { - return score::MakeUnexpected(instance->init_error_.value()); - } - return instance; - } - - /// @brief Construct an instance and start asynchronous service discovery. - /// @note Prefer Create(): This constructor is - /// public only so std::make_unique can build the object inside Create(). - explicit BasicLmControlImpl(std::string_view instance_specifier) noexcept + /// @brief Creates an inert, unconnected instance. Call init() to put it to use. + BasicLmControlImpl() noexcept = default; + + /// @brief Validate the instance specifier and start the asynchronous service discovery. + /// @param[in] instance_specifier The mw::com instance specifier identifying the Launch + /// Manager service. Must be a non-empty, valid specifier + /// string. + /// @pre Must be called exactly once, before any other method. + /// @return void once discovery is running, or the error that prevented setup. + /// @error kInvalidArguments instance_specifier is empty or malformed. + /// @error kCommunicationError the service discovery could not be started. + score::Result init(std::string_view instance_specifier) noexcept { auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); if (!specifier_result.has_value()) { LM_LOG_ERROR() << "LmControl: Invalid instance specifier" << instance_specifier << "Error: " << specifier_result.error(); - init_error_ = ExecErrc::kInvalidArguments; - return; + return score::MakeUnexpected(ExecErrc::kInvalidArguments); } auto start_result = Traits::StartFindService( @@ -124,11 +113,11 @@ class BasicLmControlImpl final : public ILmControl if (!start_result.has_value()) { LM_LOG_ERROR() << "LmControl: StartFindService failed with error" << start_result.error(); - init_error_ = ExecErrc::kCommunicationError; - return; + return score::MakeUnexpected(ExecErrc::kCommunicationError); } find_handle_ = start_result.value(); + return {}; } ~BasicLmControlImpl() noexcept override @@ -291,6 +280,9 @@ class BasicLmControlImpl final : public ILmControl /// @details mw::com may invoke this concurrently with itself. Since proxy-event /// API calls must not run concurrently on the same event, the handler /// is serialized against itself + /// @note Deliberately a separate mutex from mutex_, not a reuse of it: this path invokes the + /// user activation callback, which is free to call back into activate_run_target() and + /// therefore into mutex_. Guarding both with one non-recursive mutex would deadlock. void onActivationResult() { std::lock_guard processing_lock{sample_processing_mutex_}; @@ -309,15 +301,16 @@ class BasicLmControlImpl final : public ILmControl } /// @brief Reads and forwards up to pending_samples_count pending activation-result samples. + /// @details The loop is required as there may be more samples available than are read with a single GetNewSamples() + /// call. /// @pre The caller holds sample_processing_mutex_. - void readActivationEvents(std::size_t pending_samples_count) + void readActivationEvents(const std::size_t pending_samples_count) { std::size_t remaining = pending_samples_count; - std::size_t processed = 0U; while (remaining > 0U) { - auto get_result = proxy_->activation_result.GetNewSamples( - [this](auto sample) noexcept { + const auto get_result = proxy_->activation_result.GetNewSamples( + [this](const auto& sample) noexcept { forwardSample(sample); }, remaining); @@ -327,19 +320,18 @@ class BasicLmControlImpl final : public ILmControl break; } - const auto received = get_result.value(); - if (received == 0U) + if (get_result.value() == 0U) { // Nothing retrievable despite a positive snapshot — stop rather // than spin; any arriving new samples re-trigger the handler. break; } - processed += received; - remaining -= std::min(remaining, received); + // Cannot underflow: GetNewSamples() never hands out more than the requested maximum. + remaining -= get_result.value(); } - LM_LOG_DEBUG() << "LmControl: GetNewSamples: processed" << processed << "sample(s)"; + LM_LOG_DEBUG() << "LmControl: GetNewSamples: processed" << pending_samples_count - remaining << "sample(s)"; } /// @brief Forwards a single activation-result sample to the registered callback. @@ -368,9 +360,6 @@ class BasicLmControlImpl final : public ILmControl } } - // Error captured during construction (nullopt on success). - std::optional init_error_; - // Guards proxy_ and the service discovery handle. std::mutex mutex_; std::optional proxy_; diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp index 7d43343e6..90498b409 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -71,9 +72,18 @@ struct FakeActivationEvent { MwComMock* mock; + // Mirrors mw::com: a single GetNewSamples() call never hands out more than the + // max_sample_count agreed in Subscribe(), no matter how large the backlog is. + std::size_t subscribed_max_count{0U}; + score::Result Subscribe(std::size_t max_sample_count) { - return mock->Subscribe(max_sample_count); + auto result = mock->Subscribe(max_sample_count); + if (result.has_value()) + { + subscribed_max_count = max_sample_count; + } + return result; } void SetReceiveHandler(std::function handler) @@ -94,17 +104,21 @@ struct FakeActivationEvent template score::Result GetNewSamples(Callback&& callback, std::size_t max_count) { - auto samples_result = mock->GetNewSamples(max_count); + const std::size_t effective_max_count = std::min(max_count, subscribed_max_count); + auto samples_result = mock->GetNewSamples(effective_max_count); if (!samples_result.has_value()) { return score::MakeUnexpected(ExecErrc::kCommunicationError); } auto samples = std::move(samples_result).value(); - for (auto& sample : samples) + + // Uphold the mw::com contract even if a test stubs a larger batch than requested. + const std::size_t delivered = std::min(samples.size(), effective_max_count); + for (std::size_t i = 0U; i < delivered; ++i) { - callback(&sample); + callback(&samples[i]); } - return samples.size(); + return delivered; } }; @@ -184,6 +198,18 @@ GetActiveRunTargetResponse Available(RunTargetName name) return GetActiveRunTargetResponse{QueryStatus::kAvailable, name, ExecErrc::kGeneralError}; } +/// @brief Builds a batch of activation results, one per run target name. +std::vector MakeSamples(std::initializer_list run_target_names) +{ + std::vector samples{}; + samples.reserve(run_target_names.size()); + for (const auto name : run_target_names) + { + samples.push_back(ActivationResult{RunTargetName{name}, RunTargetActivationSource::kStateManagerRequest}); + } + return samples; +} + class LmControlUT : public ::testing::Test { protected: @@ -229,9 +255,9 @@ class LmControlUT : public ::testing::Test /// @brief Build a successfully-constructed (but not yet connected) SUT. std::unique_ptr MakeLmControl() { - auto result = LmControlImplType::Create(kValidSpecifier); - EXPECT_TRUE(result.has_value()); - return std::move(result).value(); + auto sut = std::make_unique(); + EXPECT_TRUE(sut->init(kValidSpecifier).has_value()); + return sut; } /// @brief Build an already-connected SUT using the fixture defaults. @@ -251,39 +277,46 @@ class LmControlUT : public ::testing::Test // Construction / discovery lifecycle // --------------------------------------------------------------------------- -TEST_F(LmControlUT, InvalidSpecifierYieldsInitError) +TEST_F(LmControlUT, InvalidSpecifierIsRejectedByInit) { RecordProperty( "Description", - "Create() with an empty instance specifier returns kInvalidArguments and never starts discovery."); + "init() with an empty instance specifier returns kInvalidArguments and never starts discovery."); EXPECT_CALL(mock_, StartFindService(_)).Times(0); - auto result = LmControlImplType::Create(""); + LmControlImplType sut{}; + const auto result = sut.init(""); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), ExecErrc::kInvalidArguments); } -TEST_F(LmControlUT, StartFindServiceFailureYieldsInitError) +TEST_F(LmControlUT, StartFindServiceFailureIsReportedByInit) { - RecordProperty("Description", "When StartFindService fails, Create() returns kCommunicationError."); + RecordProperty( + "Description", + "When StartFindService fails, init() returns kCommunicationError and the instance can be destroyed " + "without stopping a search it never started."); EXPECT_CALL(mock_, StartFindService(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + EXPECT_CALL(mock_, StopFindService()).Times(0); + EXPECT_CALL(mock_, Unsubscribe()).Times(0); - auto result = LmControlImplType::Create(kValidSpecifier); + LmControlImplType sut{}; + const auto result = sut.init(kValidSpecifier); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); } -TEST_F(LmControlUT, ConstructionStartsBackgroundDiscovery) +TEST_F(LmControlUT, InitStartsBackgroundDiscovery) { RecordProperty( - "Description", "Create() with a valid specifier starts asynchronous service discovery via StartFindService."); + "Description", "init() with a valid specifier starts asynchronous service discovery via StartFindService."); EXPECT_CALL(mock_, StartFindService(_)); - auto result = LmControlImplType::Create(kValidSpecifier); - EXPECT_TRUE(result.has_value()); + LmControlImplType sut{}; + EXPECT_TRUE(sut.init(kValidSpecifier).has_value()); EXPECT_TRUE(find_handler_); } @@ -585,6 +618,37 @@ TEST_F(LmControlUT, ActivationResultInvokesRegisteredCallback) EXPECT_EQ(source.value(), RunTargetActivationSource::kRecoveryAction); } +TEST_F(LmControlUT, ActivationResultBacklogExceedingSubscriptionIsDrainedInSeveralCalls) +{ + RecordProperty( + "Description", + "A backlog larger than the subscribed max_sample_count is drained with repeated GetNewSamples calls, " + "each requesting only the still-missing remainder, and every sample is forwarded exactly once."); + + auto sut = MakeConnected(); + + std::vector received{}; + ASSERT_TRUE(sut->register_run_target_activation_callback([&](RunTargetActivationSource, RunTargetName n) { + received.push_back(n); + }) + .has_value()); + + // Six pending samples, but Subscribe(kActivationResultSampleCount == 4) caps a single + // GetNewSamples call at four, so the drain loop has to come back for the remaining two. + EXPECT_CALL(mock_, GetNumNewSamplesAvailable()).WillOnce(Return(score::Result{6U})); + EXPECT_CALL(mock_, GetNewSamples(4U)).WillOnce(Invoke([](std::size_t) { + return score::Result>{MakeSamples({"T0", "T1", "T2", "T3"})}; + })); + EXPECT_CALL(mock_, GetNewSamples(2U)).WillOnce(Invoke([](std::size_t) { + return score::Result>{MakeSamples({"T4", "T5"})}; + })); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); + + EXPECT_THAT(received, ::testing::ElementsAre("T0", "T1", "T2", "T3", "T4", "T5")); +} + TEST_F(LmControlUT, ActivationResultWithoutCallbackIsDroppedSafely) { RecordProperty( From 55d6cecdadcc03062d12c0435f9a7d885df5fc95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 11:03:38 +0200 Subject: [PATCH 17/27] event read refactoring --- .../src/details/lm_control_impl.hpp | 37 ++++--------- .../src/details/lm_control_impl_UT.cpp | 52 +++++++++++-------- 2 files changed, 41 insertions(+), 48 deletions(-) diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index e9254c1bd..3ef6b9771 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -286,52 +286,37 @@ class BasicLmControlImpl final : public ILmControl void onActivationResult() { std::lock_guard processing_lock{sample_processing_mutex_}; - - // First get the number of samples available. - // This way we can have a bounded loop to retrieve all samples. - // Relying purely on the return of GetNewSamples() would yield a possibly unbounded loop. - auto available_result = proxy_->activation_result.GetNumNewSamplesAvailable(); - if (!available_result.has_value()) - { - LM_LOG_ERROR() << "LmControl: GetNumNewSamplesAvailable failed with error:" << available_result.error(); - return; - } - - readActivationEvents(available_result.value()); + readActivationEvents(); } - /// @brief Reads and forwards up to pending_samples_count pending activation-result samples. - /// @details The loop is required as there may be more samples available than are read with a single GetNewSamples() - /// call. + /// @brief Ready and forwards all pending activation-result samples. /// @pre The caller holds sample_processing_mutex_. - void readActivationEvents(const std::size_t pending_samples_count) + void readActivationEvents() { - std::size_t remaining = pending_samples_count; - while (remaining > 0U) + std::size_t processed = 0U; + for (;;) { const auto get_result = proxy_->activation_result.GetNewSamples( [this](const auto& sample) noexcept { forwardSample(sample); }, - remaining); + kActivationResultSampleCount); if (!get_result.has_value()) { LM_LOG_ERROR() << "LmControl: GetNewSamples failed with error:" << get_result.error(); break; } - if (get_result.value() == 0U) + processed += get_result.value(); + + // All samples consumed, if the batch was smaller than the max. + if (get_result.value() < kActivationResultSampleCount) { - // Nothing retrievable despite a positive snapshot — stop rather - // than spin; any arriving new samples re-trigger the handler. break; } - - // Cannot underflow: GetNewSamples() never hands out more than the requested maximum. - remaining -= get_result.value(); } - LM_LOG_DEBUG() << "LmControl: GetNewSamples: processed" << pending_samples_count - remaining << "sample(s)"; + LM_LOG_DEBUG() << "LmControl: GetNewSamples: processed" << processed << "sample(s)"; } /// @brief Forwards a single activation-result sample to the registered callback. diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp index 90498b409..82f20aaa5 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp @@ -60,7 +60,6 @@ class MwComMock MOCK_METHOD(score::Result, Subscribe, (std::size_t max_sample_count)); MOCK_METHOD(void, SetReceiveHandler, (std::function handler)); MOCK_METHOD(void, Unsubscribe, ()); - MOCK_METHOD(score::Result, GetNumNewSamplesAvailable, ()); MOCK_METHOD((score::Result>), GetNewSamples, (std::size_t max_count)); MOCK_METHOD(score::Result, ActivateRunTarget, (const ActivateRunTargetRequest& request)); @@ -96,11 +95,6 @@ struct FakeActivationEvent mock->Unsubscribe(); } - score::Result GetNumNewSamplesAvailable() - { - return mock->GetNumNewSamplesAvailable(); - } - template score::Result GetNewSamples(Callback&& callback, std::size_t max_count) { @@ -228,7 +222,6 @@ class LmControlUT : public ::testing::Test ON_CALL(mock_, CreateProxy()).WillByDefault(Return(score::Result{})); ON_CALL(mock_, Subscribe(_)).WillByDefault(Return(score::Result{})); ON_CALL(mock_, SetReceiveHandler(_)).WillByDefault(SaveArg<0>(&receive_handler_)); - ON_CALL(mock_, GetNumNewSamplesAvailable()).WillByDefault(Return(score::Result{0U})); ON_CALL(mock_, GetNewSamples(_)).WillByDefault(Invoke([](std::size_t) { return score::Result>{std::vector{}}; })); @@ -603,7 +596,6 @@ TEST_F(LmControlUT, ActivationResultInvokesRegisteredCallback) }) .has_value()); - EXPECT_CALL(mock_, GetNumNewSamplesAvailable()).WillOnce(Return(score::Result{1U})); EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Invoke([](std::size_t) { std::vector samples; samples.push_back(ActivationResult{RunTargetName{"Recovery"}, RunTargetActivationSource::kRecoveryAction}); @@ -622,8 +614,8 @@ TEST_F(LmControlUT, ActivationResultBacklogExceedingSubscriptionIsDrainedInSever { RecordProperty( "Description", - "A backlog larger than the subscribed max_sample_count is drained with repeated GetNewSamples calls, " - "each requesting only the still-missing remainder, and every sample is forwarded exactly once."); + "A backlog larger than the subscribed max_sample_count is drained with repeated GetNewSamples calls " + "until a batch smaller than the maximum signals an empty queue; every sample is forwarded exactly once."); auto sut = MakeConnected(); @@ -633,20 +625,39 @@ TEST_F(LmControlUT, ActivationResultBacklogExceedingSubscriptionIsDrainedInSever }) .has_value()); - // Six pending samples, but Subscribe(kActivationResultSampleCount == 4) caps a single - // GetNewSamples call at four, so the drain loop has to come back for the remaining two. - EXPECT_CALL(mock_, GetNumNewSamplesAvailable()).WillOnce(Return(score::Result{6U})); + // Nine pending samples, but Subscribe(kActivationResultSampleCount == 4) caps every single + // GetNewSamples call at four: two full batches, then a short one that ends the drain. + EXPECT_CALL(mock_, GetNewSamples(4U)) + .WillOnce(Invoke([](std::size_t) { + return score::Result>{MakeSamples({"T0", "T1", "T2", "T3"})}; + })) + .WillOnce(Invoke([](std::size_t) { + return score::Result>{MakeSamples({"T4", "T5", "T6", "T7"})}; + })) + .WillOnce(Invoke([](std::size_t) { + return score::Result>{MakeSamples({"T8"})}; + })); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); + + EXPECT_THAT(received, ::testing::ElementsAre("T0", "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8")); +} + +TEST_F(LmControlUT, ActivationResultDrainStopsOnEmptyBatch) +{ + RecordProperty( + "Description", + "A receive handler firing with nothing retrievable issues exactly one GetNewSamples call and does not spin."); + + auto sut = MakeConnected(); + EXPECT_CALL(mock_, GetNewSamples(4U)).WillOnce(Invoke([](std::size_t) { - return score::Result>{MakeSamples({"T0", "T1", "T2", "T3"})}; - })); - EXPECT_CALL(mock_, GetNewSamples(2U)).WillOnce(Invoke([](std::size_t) { - return score::Result>{MakeSamples({"T4", "T5"})}; + return score::Result>{std::vector{}}; })); ASSERT_TRUE(receive_handler_); receive_handler_(); - - EXPECT_THAT(received, ::testing::ElementsAre("T0", "T1", "T2", "T3", "T4", "T5")); } TEST_F(LmControlUT, ActivationResultWithoutCallbackIsDroppedSafely) @@ -656,7 +667,6 @@ TEST_F(LmControlUT, ActivationResultWithoutCallbackIsDroppedSafely) auto sut = MakeConnected(); - EXPECT_CALL(mock_, GetNumNewSamplesAvailable()).WillOnce(Return(score::Result{1U})); EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Invoke([](std::size_t) { std::vector samples; samples.push_back(ActivationResult{RunTargetName{"Running"}, RunTargetActivationSource::kStateManagerRequest}); @@ -674,7 +684,6 @@ TEST_F(LmControlUT, ActivationResultGetNewSamplesFailureIsTolerated) auto sut = MakeConnected(); - EXPECT_CALL(mock_, GetNumNewSamplesAvailable()).WillOnce(Return(score::Result{1U})); EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); ASSERT_TRUE(receive_handler_); @@ -698,7 +707,6 @@ TEST_F(LmControlUT, LatestRegisteredCallbackWins) }) .has_value()); - EXPECT_CALL(mock_, GetNumNewSamplesAvailable()).WillOnce(Return(score::Result{1U})); EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Invoke([](std::size_t) { std::vector samples; samples.push_back(ActivationResult{RunTargetName{"Running"}, RunTargetActivationSource::kStateManagerRequest}); From 0539aa14897b042adbd5a3334a4bf0f0c794afe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 11:10:10 +0200 Subject: [PATCH 18/27] Refactor callback invocation --- .../src/details/lm_control_impl.hpp | 42 ++++++++++++------- .../src/details/lm_control_impl_UT.cpp | 3 +- .../src/lm_control/src/ilm_control.hpp | 8 +--- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index 3ef6b9771..a79168bbd 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -290,15 +290,24 @@ class BasicLmControlImpl final : public ILmControl } /// @brief Ready and forwards all pending activation-result samples. + /// @details The callback is copied once here rather than once per sample: a registration + /// racing with a running drain takes effect from the next handler invocation on, + /// never mid-drain. /// @pre The caller holds sample_processing_mutex_. void readActivationEvents() { + const ActivationCallback callback = currentCallback(); + if (!callback) + { + LM_LOG_WARN() << "LmControl: activation_result: no callback registered — samples dropped"; + } + std::size_t processed = 0U; for (;;) { const auto get_result = proxy_->activation_result.GetNewSamples( - [this](const auto& sample) noexcept { - forwardSample(sample); + [&callback](const auto& sample) noexcept { + forwardSample(callback, sample); }, kActivationResultSampleCount); if (!get_result.has_value()) @@ -319,30 +328,33 @@ class BasicLmControlImpl final : public ILmControl LM_LOG_DEBUG() << "LmControl: GetNewSamples: processed" << processed << "sample(s)"; } - /// @brief Forwards a single activation-result sample to the registered callback. + /// @brief Returns a copy of the currently registered callback, empty if there is none. + /// @details Copied out under the lock so that the caller can invoke it after unlocking: + /// holding callback_mutex_ across the invocation would deadlock a callback that + /// re-registers itself. + ActivationCallback currentCallback() noexcept + { + std::lock_guard lock{callback_mutex_}; + return callback_; + } + + /// @brief Forwards a single activation-result sample to the given callback. /// @details The sample parameter is intentionally a template: in production it /// is a score::mw::com::SamplePtr, while a fake proxy /// in tests can hand in any pointer-like sample. Nothing here depends /// on the concrete mw::com sample type. + /// @param[in] callback The callback to invoke, may be empty in which case the sample is dropped. + /// @param[in] sample The received activation result. template - void forwardSample(const SamplePtrType& sample) noexcept + static void forwardSample(const ActivationCallback& callback, const SamplePtrType& sample) noexcept { LM_LOG_DEBUG() << "LmControl: activation_result received: run_target=" << sample->activated_run_target << "source=" << toStringView(sample->activation_source); - ActivationCallback cb; - { - std::lock_guard lock{callback_mutex_}; - cb = callback_; - } - if (cb) + if (callback) { - cb(sample->activation_source, sample->activated_run_target); + callback(sample->activation_source, sample->activated_run_target); LM_LOG_DEBUG() << "LmControl: activation_result: user callback invoked"; } - else - { - LM_LOG_WARN() << "LmControl: activation_result: no callback registered — sample dropped"; - } } // Guards proxy_ and the service discovery handle. diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp index 82f20aaa5..08de8b52b 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp @@ -273,8 +273,7 @@ class LmControlUT : public ::testing::Test TEST_F(LmControlUT, InvalidSpecifierIsRejectedByInit) { RecordProperty( - "Description", - "init() with an empty instance specifier returns kInvalidArguments and never starts discovery."); + "Description", "init() with an empty instance specifier returns kInvalidArguments and never starts discovery."); EXPECT_CALL(mock_, StartFindService(_)).Times(0); diff --git a/score/launch_manager/src/lm_control/src/ilm_control.hpp b/score/launch_manager/src/lm_control/src/ilm_control.hpp index b03cb14f1..2c2c6e6d7 100644 --- a/score/launch_manager/src/lm_control/src/ilm_control.hpp +++ b/score/launch_manager/src/lm_control/src/ilm_control.hpp @@ -131,17 +131,13 @@ class ILmControl /// Only a single subscriber is supported. The expected usage is /// register-once during setup, leaving the callback in place until /// the ILmControl instance is destroyed. Re-registration is - /// supported defensively (a second call overrides the previous - /// setting) but callers that re-register must be prepared to - /// handle kCallbackInProgress (transient, retry). There is no - /// un-register path; empty callbacks are rejected. + /// supported defensively: a second call overrides the previous + /// setting. There is no un-register path; empty callbacks are rejected. /// /// @param[in] callback The callback to invoke when Run Target activation finishes. /// /// @returns void on success. /// - /// @error kCallbackInProgress A new callback cannot be registered because the - /// current callback is still executing. Retry after it returns. /// @error kInvalidArguments The callback is empty. virtual score::Result register_run_target_activation_callback(ActivationCallback callback) = 0; From 52ce0ba55003d833e581332213687bad064583c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 11:12:36 +0200 Subject: [PATCH 19/27] Remove LmControlServer --- .../src/daemon/src/control/BUILD | 16 -- .../daemon/src/control/lm_control_server.cpp | 176 ------------------ .../daemon/src/control/lm_control_server.hpp | 106 ----------- 3 files changed, 298 deletions(-) delete mode 100644 score/launch_manager/src/daemon/src/control/lm_control_server.cpp delete mode 100644 score/launch_manager/src/daemon/src/control/lm_control_server.hpp diff --git a/score/launch_manager/src/daemon/src/control/BUILD b/score/launch_manager/src/daemon/src/control/BUILD index fd6f91d3e..090ccec7d 100644 --- a/score/launch_manager/src/daemon/src/control/BUILD +++ b/score/launch_manager/src/daemon/src/control/BUILD @@ -12,22 +12,6 @@ # ******************************************************************************* load("@rules_cc//cc:defs.bzl", "cc_library") -cc_library( - name = "lm_control_server", - srcs = ["lm_control_server.cpp"], - hdrs = ["lm_control_server.hpp"], - include_prefix = "score/mw/launch_manager/control", - strip_include_prefix = "/score/launch_manager/src/daemon/src/control", - visibility = [ - "//score/launch_manager/src/daemon:__pkg__", - "//tests:__subpackages__", - ], - deps = [ - "//score/launch_manager/src/daemon/src/common:log", - "//score/launch_manager/src/lm_control:lm_control_service", - ], -) - cc_library( name = "control_client_channel", srcs = [ diff --git a/score/launch_manager/src/daemon/src/control/lm_control_server.cpp b/score/launch_manager/src/daemon/src/control/lm_control_server.cpp deleted file mode 100644 index ef060402f..000000000 --- a/score/launch_manager/src/daemon/src/control/lm_control_server.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#include "lm_control_server.hpp" - -#include - -#include "score/mw/launch_manager/common/log.hpp" - -namespace score::mw::launch_manager::control -{ - -namespace -{ -constexpr std::string_view kDefaultInstanceSpecifier = "LaunchManager/StateManager/Instance"; -constexpr const char* kInstanceSpecifierEnvVar = "SCORE_LCM_SKELETON_INSTANCE_SPECIFIER"; -} // namespace - -LmControlServer::LmControlServer(IGraph& graph, std::string_view instance_specifier) - : graph_{graph} - , instance_specifier_{instance_specifier.empty() - ? (std::getenv(kInstanceSpecifierEnvVar) != nullptr - ? std::getenv(kInstanceSpecifierEnvVar) - : std::string{kDefaultInstanceSpecifier}) - : std::string{instance_specifier}} - , skeleton_{} -{ -} - -LmControlServer::~LmControlServer() noexcept -{ - Shutdown(); -} - -bool LmControlServer::Initialize() -{ - auto specifier_result = score::mw::com::InstanceSpecifier::Create(instance_specifier_); - if (!specifier_result.has_value()) - { - LM_LOG_ERROR() << "LmControlServer: invalid instance specifier: " << instance_specifier_; - return false; - } - - auto create_result = lifecycle::LmControlSkeleton::Create(specifier_result.value()); - if (!create_result.has_value()) - { - LM_LOG_ERROR() << "LmControlServer: failed to create skeleton"; - return false; - } - - skeleton_.emplace(std::move(create_result).value()); - - /* ToDo: - * do we have signature mismatches? - * do we needs this lambda to register handler? - * can we pass this class method to the RegisterHandler() method (or use std function)? */ - - /*skeleton_->activate_run_target.RegisterHandler( - [this](const lifecycle::ActivateRunTargetRequest& request) { - return OnActivateRunTarget(request); - });*/ - skeleton_->activate_run_target.RegisterHandler( - [this](lifecycle::ActivateRunTargetResponse& response, - const lifecycle::ActivateRunTargetRequest& request) { - response = OnActivateRunTarget(request); - }); - - /*skeleton_->get_active_run_target.RegisterHandler( - [this]() { - return OnGetActiveRunTarget(); - });*/ - skeleton_->get_active_run_target.RegisterHandler( - [this](lifecycle::GetActiveRunTargetResponse& response) { - response = OnGetActiveRunTarget(); - }); - - auto offer_result = skeleton_->OfferService(); - if (!offer_result.has_value()) - { - LM_LOG_ERROR() << "LmControlServer: OfferService failed"; - skeleton_.reset(); - return false; - } - - LM_LOG_INFO() << "LmControlServer: offering service on " << instance_specifier_; - return true; -} - -void LmControlServer::Shutdown() -{ - if (skeleton_.has_value()) - { - skeleton_->StopOfferService(); - skeleton_.reset(); - LM_LOG_INFO() << "LmControlServer: service stopped"; - } -} - -lifecycle::ActivateRunTargetResponse LmControlServer::OnActivateRunTarget( - const lifecycle::ActivateRunTargetRequest& request) -{ - LM_LOG_INFO() << "LmControlServer: ActivateRunTarget run_target=" << request.run_target_name; - - const bool force = (request.mode == lifecycle::ActivationMode::kForced); - const auto request_id = next_request_id_++; - const bool accepted = graph_.enqueueRunTargetActivation( - RunTargetRequest{request.run_target_name, force, request_id}); - - if (!accepted) - { - LM_LOG_ERROR() << "LmControlServer: ActivateRunTarget rejected run_target=" - << request.run_target_name; - return lifecycle::ActivateRunTargetResponse{ - lifecycle::RequestStatus::kRejected, - lifecycle::ExecErrc::kGeneralError}; - } - - return lifecycle::ActivateRunTargetResponse{ - lifecycle::RequestStatus::kAccepted, - lifecycle::ExecErrc::kGeneralError // unused when kAccepted - }; -} - -lifecycle::GetActiveRunTargetResponse LmControlServer::OnGetActiveRunTarget() -{ - const auto active = graph_.getActiveRunTarget(); - if (!active.has_value()) - { - LM_LOG_DEBUG() << "LmControlServer: GetActiveRunTarget — activation in progress"; - return lifecycle::GetActiveRunTargetResponse{ - lifecycle::QueryStatus::kNotAvailable, - lifecycle::RunTargetName{}, - lifecycle::ExecErrc::kActivationInProgress}; - } - - LM_LOG_DEBUG() << "LmControlServer: GetActiveRunTarget — returning " << active.value(); - return lifecycle::GetActiveRunTargetResponse{ - lifecycle::QueryStatus::kAvailable, - lifecycle::RunTargetName{active.value()}, - lifecycle::ExecErrc::kGeneralError // unused when kAvailable - }; -} - -void LmControlServer::activationCompleted(std::string_view runTarget, std::optional requestId) -{ - if (!skeleton_.has_value()) - { - LM_LOG_ERROR() << "LmControlServer: activationCompleted — skeleton not available, event not sent"; - return; - } - - auto alloc = skeleton_->activation_result.Allocate(); - if (!alloc.has_value()) - { - LM_LOG_ERROR() << "LmControlServer: activationCompleted — Allocate() failed, event not sent"; - return; - } - - alloc.value().Get()->activated_run_target = lifecycle::RunTargetName{runTarget}; - alloc.value().Get()->activation_source = - requestId.has_value() ? lifecycle::RunTargetActivationSource::kStateManagerRequest - : lifecycle::RunTargetActivationSource::kRecoveryAction; - skeleton_->activation_result.Send(std::move(alloc).value()); -} - -} // namespace score::mw::launch_manager::control diff --git a/score/launch_manager/src/daemon/src/control/lm_control_server.hpp b/score/launch_manager/src/daemon/src/control/lm_control_server.hpp deleted file mode 100644 index d0d52c6f8..000000000 --- a/score/launch_manager/src/daemon/src/control/lm_control_server.hpp +++ /dev/null @@ -1,106 +0,0 @@ -/******************************************************************************** - * 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 SCORE_MW_LAUNCH_MANAGER_CONTROL_LM_CONTROL_SERVER_HPP -#define SCORE_MW_LAUNCH_MANAGER_CONTROL_LM_CONTROL_SERVER_HPP - -#include -#include -#include - -#include "score/mw/lifecycle/lm_control_service.h" - -namespace score::mw::launch_manager::control -{ - -/// @brief Internal request to activate a Run Target, correlated to its -/// completion via requestId. -struct RunTargetRequest -{ - lifecycle::RunTargetName runTargetName; - bool force; - std::int32_t requestId; -}; - -/// @brief Abstraction over the process graph that LmControlServer delegates to. -class IGraph -{ - public: - virtual ~IGraph() noexcept = default; - - /// @return the currently active Run Target, or nullopt while an - /// activation is in progress. - virtual std::optional getActiveRunTarget() = 0; - - /// @return true if the request was accepted/enqueued, false if rejected. - virtual bool enqueueRunTargetActivation(RunTargetRequest request) = 0; - - protected: - IGraph() = default; - IGraph(const IGraph&) = default; - IGraph& operator=(const IGraph&) = default; - IGraph(IGraph&&) = default; - IGraph& operator=(IGraph&&) = default; -}; - -/// @brief Hosts the mw::com LmControl skeleton on the Launch Manager side. -/// -/// Offers the LmControlService over mw::com so that State Managers can call -/// activate_run_target and get_active_run_target via the proxy. Delegates -/// the actual Run Target querying and activation to an injected IGraph. -class LmControlServer final -{ - public: - /// @param graph The graph to delegate Run Target queries and - /// activations to. Must outlive this instance. - /// @param instance_specifier The mw::com instance specifier for the - /// skeleton. Reads SCORE_LCM_SKELETON_INSTANCE_SPECIFIER - /// from the environment if empty. - explicit LmControlServer(IGraph& graph, std::string_view instance_specifier = {}); - - ~LmControlServer() noexcept; - - LmControlServer(const LmControlServer&) = delete; - LmControlServer& operator=(const LmControlServer&) = delete; - LmControlServer(LmControlServer&&) = delete; - LmControlServer& operator=(LmControlServer&&) = delete; - - /// @brief Create the skeleton, register handlers, and offer the service. - /// @return true on success, false if skeleton creation or OfferService failed. - bool Initialize(); - - /// @brief Stop offering the service and destroy the skeleton. - void Shutdown(); - - /// @brief Notify that a Run Target activation has settled. Sends the - /// activation_result event to all subscribed proxies. - /// @param runTarget The Run Target the graph settled on. - /// @param requestId The id of the request that triggered this activation, - /// or nullopt if the activation was not tied to a - /// specific request (e.g. a recovery action). - void activationCompleted(std::string_view runTarget, std::optional requestId); - - private: - lifecycle::ActivateRunTargetResponse OnActivateRunTarget( - const lifecycle::ActivateRunTargetRequest& request); - - lifecycle::GetActiveRunTargetResponse OnGetActiveRunTarget(); - - IGraph& graph_; - std::string instance_specifier_; - std::optional skeleton_; - std::int32_t next_request_id_{0}; -}; - -} // namespace score::mw::launch_manager::control - -#endif // SCORE_MW_LAUNCH_MANAGER_CONTROL_LM_CONTROL_SERVER_HPP From faa5d86ef37e06efa1e952c7705e9112da85d080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 11:13:29 +0200 Subject: [PATCH 20/27] Cleanup --- score/launch_manager/src/execution_error.h | 8 ++----- score/launch_manager/src/lm_control/BUILD | 24 ------------------- .../lm_control/src/details/ilm_control_UT.cpp | 10 -------- .../src/lm_control/src/ilm_control.hpp | 1 + 4 files changed, 3 insertions(+), 40 deletions(-) diff --git a/score/launch_manager/src/execution_error.h b/score/launch_manager/src/execution_error.h index d65235b7d..6139f0858 100644 --- a/score/launch_manager/src/execution_error.h +++ b/score/launch_manager/src/execution_error.h @@ -42,10 +42,8 @@ enum class ExecErrc : score::result::ErrorCode kCycleOverrun = 13, ///< Deterministic activation cycle time exceeded kActivationInProgress = 14, ///< A Run Target activation is already in progress; no single Run Target is currently active - kCallbackInProgress = 15, ///< A new callback cannot be registered because a previously registered callback is - ///< currently being executed - kRequestQueueIsFull = 16, ///< The activation request queue is full; the request was discarded - kRunTargetDoesntExist = 17 ///< The requested Run Target name does not exist in the current configuration + kRequestQueueIsFull = 15, ///< The activation request queue is full; the request was discarded + kRunTargetDoesntExist = 16 ///< The requested Run Target name does not exist in the current configuration }; class ExecErrorDomain final : public score::result::ErrorDomain @@ -85,8 +83,6 @@ class ExecErrorDomain final : public score::result::ErrorDomain return "Deterministic activation cycle time exceeded"; case ExecErrc::kActivationInProgress: return "A Run Target activation is already in progress; no single Run Target is currently active"; - case ExecErrc::kCallbackInProgress: - return "A new callback cannot be registered because the current callback is still executing"; case ExecErrc::kRequestQueueIsFull: return "The activation request queue is full; the request was discarded"; case ExecErrc::kRunTargetDoesntExist: diff --git a/score/launch_manager/src/lm_control/BUILD b/score/launch_manager/src/lm_control/BUILD index 507fba94a..e7262795a 100644 --- a/score/launch_manager/src/lm_control/BUILD +++ b/score/launch_manager/src/lm_control/BUILD @@ -12,19 +12,6 @@ # ******************************************************************************* load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") -cc_library( - name = "lm_control_service", - hdrs = ["src/details/lm_control_service.h"], - include_prefix = "score/mw/lifecycle", - strip_include_prefix = "/score/launch_manager/src/lm_control/src/details", - visibility = ["//score:__subpackages__"], - deps = [ - "//score/launch_manager:error", - "//score/launch_manager/src/lm_control:lm_control", - "@score_communication//score/mw/com", - ], -) - cc_library( name = "lm_control", srcs = [ @@ -51,12 +38,6 @@ cc_library( ], ) -# --------------------------------------------------------------------------- -# Unit test: BasicLmControlImpl driven through a fake proxy injected at compile -# time. Lives next to the implementation it covers. No mw::com runtime, shared -# memory, or manifest required. -# --------------------------------------------------------------------------- - cc_test( name = "lm_control_impl_UT", srcs = ["src/details/lm_control_impl_UT.cpp"], @@ -69,11 +50,6 @@ cc_test( ], ) -# --------------------------------------------------------------------------- -# Unit test: ILmControl::Create factory. Runs against a mw::com runtime -# initialized from the shared manifest; no provider skeleton is required. -# --------------------------------------------------------------------------- - cc_test( name = "ilm_control_UT", srcs = ["src/details/ilm_control_UT.cpp"], diff --git a/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp b/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp index e8e97955c..021ce56cc 100644 --- a/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp +++ b/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp @@ -11,16 +11,6 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/// @file -/// @brief Unit test for the ILmControl::Create factory in ilm_control.cpp. -/// -/// ILmControl::Create builds the production BasicLmControlImpl wired to the real -/// mw::com proxy, so there is no compile-time seam here. The test therefore runs -/// against a mw::com runtime initialized from the manifest passed on the command -/// line via --service_instance_manifest (see main()); a valid specifier only -/// needs StartFindService to succeed (background discovery), so no provider -/// skeleton is required for Create() to hand back a usable instance. - #include #include diff --git a/score/launch_manager/src/lm_control/src/ilm_control.hpp b/score/launch_manager/src/lm_control/src/ilm_control.hpp index 2c2c6e6d7..b72608391 100644 --- a/score/launch_manager/src/lm_control/src/ilm_control.hpp +++ b/score/launch_manager/src/lm_control/src/ilm_control.hpp @@ -89,6 +89,7 @@ class ILmControl /// @returns A unique_ptr to the ILmControl instance on success. /// /// @error kInvalidArguments instance_specifier is empty or malformed. + /// @error kCommunicationError The Launch Manager service could not be reached. static score::Result> Create(std::string_view instance_specifier); /// @brief Virtual destructor for safe deletion through this interface. From 45bfd317c567c6ab56ddb02fb9ebc97c94b25e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 11:48:33 +0200 Subject: [PATCH 21/27] Fix compiler warning and tsan failure --- score/launch_manager/src/execution_error.h | 4 ++-- score/launch_manager/src/lm_control/src/fixed_string.hpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/score/launch_manager/src/execution_error.h b/score/launch_manager/src/execution_error.h index 6139f0858..63f2659db 100644 --- a/score/launch_manager/src/execution_error.h +++ b/score/launch_manager/src/execution_error.h @@ -93,12 +93,12 @@ class ExecErrorDomain final : public score::result::ErrorDomain } }; -constexpr ExecErrorDomain g_ExecErrorDomain{}; +/// @brief The single ExecErrorDomain instance every ExecErrc-based Error refers to. +inline constexpr ExecErrorDomain g_ExecErrorDomain{}; constexpr score::result::Error MakeError(ExecErrc code, const std::string_view user_message = "") noexcept { return score::result::Error{static_cast(code), g_ExecErrorDomain, user_message}; - ; } } // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/fixed_string.hpp b/score/launch_manager/src/lm_control/src/fixed_string.hpp index af67ca02f..094cd9b67 100644 --- a/score/launch_manager/src/lm_control/src/fixed_string.hpp +++ b/score/launch_manager/src/lm_control/src/fixed_string.hpp @@ -364,7 +364,7 @@ bool operator!=(const FixedString& lhs, const FixedString& rhs) noexcept template Stream& operator<<(Stream& os, const FixedString& fs) { - return os << fs.data(); + return os << fs.as_string_view(); } } // namespace score::mw::lifecycle From 6a83681631b5a4c214b48c84976490029c08a736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 11:51:23 +0200 Subject: [PATCH 22/27] Format fix --- MODULE.bazel | 1 + score/launch_manager/src/lm_control/BUILD | 1 - .../src/details/lm_control_service.h | 27 +++++++++---------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index d14d41902..6f82231a1 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -181,6 +181,7 @@ git_override( commit = "2daf5b88111d8f6e7f5516f240d4eacc67ceb361", remote = "https://github.com/eclipse-score/baselibs.git", ) + bazel_dep(name = "score_communication") git_override( module_name = "score_communication", diff --git a/score/launch_manager/src/lm_control/BUILD b/score/launch_manager/src/lm_control/BUILD index e7262795a..f3c0c7bee 100644 --- a/score/launch_manager/src/lm_control/BUILD +++ b/score/launch_manager/src/lm_control/BUILD @@ -64,4 +64,3 @@ cc_test( "@score_communication//score/mw/com", ], ) - diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_service.h b/score/launch_manager/src/lm_control/src/details/lm_control_service.h index bdf658f0b..e760115da 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_service.h +++ b/score/launch_manager/src/lm_control/src/details/lm_control_service.h @@ -32,9 +32,9 @@ namespace score::mw::lifecycle /// underlying storage is `uint8_t` to keep the wire representation compact. enum class ActivationMode : uint8_t { - kQueued = 0, ///< Queue this request behind any in-progress activation. - kForced = 1, ///< Cancel any in-progress activation and the pending queue, - ///< then start this activation immediately. + kQueued = 0, ///< Queue this request behind any in-progress activation. + kForced = 1, ///< Cancel any in-progress activation and the pending queue, + ///< then start this activation immediately. }; /// @brief Request sent by the State Manager to activate a Run Target. @@ -50,8 +50,8 @@ struct ActivateRunTargetRequest /// @brief Indicates whether the request was accepted or rejected by the Launch Manager. enum class RequestStatus : uint8_t { - kAccepted = 0, ///< The request was accepted. - kRejected = 1, ///< The request was rejected; see the accompanying rejection_reason field. + kAccepted = 0, ///< The request was accepted. + kRejected = 1, ///< The request was rejected; see the accompanying rejection_reason field. }; /// @brief Synchronous response to an activate_run_target request. @@ -74,8 +74,8 @@ struct ActivateRunTargetResponse /// @brief Indicates whether the Launch Manager can provide the currently active Run Target. enum class QueryStatus : uint8_t { - kAvailable = 0, ///< A settled Run Target exists; see the run_target field. - kNotAvailable = 1, ///< No settled Run Target; an activation is in progress. + kAvailable = 0, ///< A settled Run Target exists; see the run_target field. + kNotAvailable = 1, ///< No settled Run Target; an activation is in progress. }; /// @brief Synchronous response to a get_active_run_target query. @@ -135,8 +135,9 @@ class LmControlService : public Trait::Base /// the request was accepted or rejected (e.g. queue full, unknown Run Target). /// If accepted, the actual activation result arrives asynchronously via /// the `activation_result` event. - typename Trait::template Method - activate_run_target{*this, "ActivateRunTarget"}; + typename Trait::template Method activate_run_target{ + *this, + "ActivateRunTarget"}; /// @brief Query the currently active Run Target. /// @@ -144,18 +145,16 @@ class LmControlService : public Trait::Base /// in progress, status is QueryStatus::kNotAvailable and rejection_reason /// carries kActivationInProgress. The caller should wait for the /// activation_result event and retry if needed. - typename Trait::template Method - get_active_run_target{*this, "GetActiveRunTarget"}; + typename Trait::template Method get_active_run_target{*this, "GetActiveRunTarget"}; /// @brief Event fired by the Launch Manager when a Run Target activation settles. /// /// All connected proxies receive this event. Each subscriber's registered /// ActivationCallback is invoked with the settled Run Target and source. - typename Trait::template Event - activation_result{*this, "ActivationResult"}; + typename Trait::template Event activation_result{*this, "ActivationResult"}; }; -using LmControlProxy = score::mw::com::AsProxy; +using LmControlProxy = score::mw::com::AsProxy; using LmControlSkeleton = score::mw::com::AsSkeleton; } // namespace score::mw::lifecycle From fa8d9e94bb3cde1aa4321c401c4c30a05a887af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 13:25:27 +0200 Subject: [PATCH 23/27] Fix compiler error --- .../src/details/lm_control_impl.hpp | 17 +++++++++---- .../src/details/lm_control_impl_UT.cpp | 25 ++++++++++++++++--- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index a79168bbd..5f677e756 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -104,7 +104,7 @@ class BasicLmControlImpl final : public ILmControl return score::MakeUnexpected(ExecErrc::kInvalidArguments); } - auto start_result = Traits::StartFindService( + const auto start_result = Traits::StartFindService( [this](score::mw::com::ServiceHandleContainer handles, FindServiceHandle) noexcept { onServiceFound(std::move(handles)); }, @@ -153,7 +153,7 @@ class BasicLmControlImpl final : public ILmControl const ActivateRunTargetRequest request{ runTargetName, force ? ActivationMode::kForced : ActivationMode::kQueued}; - auto result = proxy->activate_run_target(request); + const auto result = proxy->activate_run_target(request); if (!result.has_value()) { LM_LOG_ERROR() << "LmControl: activate_run_target: proxy method call failed with error:" << result.error(); @@ -189,7 +189,7 @@ class BasicLmControlImpl final : public ILmControl return score::MakeUnexpected(ExecErrc::kCommunicationError); } - auto result = proxy->get_active_run_target(); + const auto result = proxy->get_active_run_target(); if (!result.has_value()) { LM_LOG_ERROR() << "LmControl: get_active_run_target: proxy method call failed with error:" @@ -262,7 +262,7 @@ class BasicLmControlImpl final : public ILmControl /// @return true on success; on failure proxy_ is reset so we stay unconnected. bool subscribeToActivationResults() noexcept { - auto subscribe_result = proxy_->activation_result.Subscribe(kActivationResultSampleCount); + const auto subscribe_result = proxy_->activation_result.Subscribe(kActivationResultSampleCount); if (!subscribe_result.has_value()) { LM_LOG_ERROR() << "LmControl: activation_result.Subscribe failed with error:" << subscribe_result.error(); @@ -270,9 +270,16 @@ class BasicLmControlImpl final : public ILmControl return false; } - proxy_->activation_result.SetReceiveHandler([this]() { + const auto receivehandler_result = proxy_->activation_result.SetReceiveHandler([this]() { onActivationResult(); }); + if (!receivehandler_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: activation_result.SetReceiveHandler failed with error:" + << receivehandler_result.error(); + proxy_.reset(); + return false; + } return true; } diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp index 08de8b52b..8923c4d89 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp @@ -58,7 +58,7 @@ class MwComMock MOCK_METHOD(score::Result, CreateProxy, ()); MOCK_METHOD(score::Result, Subscribe, (std::size_t max_sample_count)); - MOCK_METHOD(void, SetReceiveHandler, (std::function handler)); + MOCK_METHOD(score::Result, SetReceiveHandler, (std::function handler)); MOCK_METHOD(void, Unsubscribe, ()); MOCK_METHOD((score::Result>), GetNewSamples, (std::size_t max_count)); @@ -85,9 +85,9 @@ struct FakeActivationEvent return result; } - void SetReceiveHandler(std::function handler) + score::Result SetReceiveHandler(std::function handler) { - mock->SetReceiveHandler(std::move(handler)); + return mock->SetReceiveHandler(std::move(handler)); } void Unsubscribe() @@ -221,7 +221,8 @@ class LmControlUT : public ::testing::Test ON_CALL(mock_, StopFindService()).WillByDefault(Return(score::Result{})); ON_CALL(mock_, CreateProxy()).WillByDefault(Return(score::Result{})); ON_CALL(mock_, Subscribe(_)).WillByDefault(Return(score::Result{})); - ON_CALL(mock_, SetReceiveHandler(_)).WillByDefault(SaveArg<0>(&receive_handler_)); + ON_CALL(mock_, SetReceiveHandler(_)) + .WillByDefault(DoAll(SaveArg<0>(&receive_handler_), Return(score::Result{}))); ON_CALL(mock_, GetNewSamples(_)).WillByDefault(Invoke([](std::size_t) { return score::Result>{std::vector{}}; })); @@ -427,6 +428,22 @@ TEST_F(LmControlUT, SubscribeFailureLeavesInstanceDisconnected) EXPECT_EQ(get.error(), ExecErrc::kCommunicationError); } +TEST_F(LmControlUT, SetReceiveHandlerFailureLeavesInstanceDisconnected) +{ + RecordProperty( + "Description", + "When installing the receive handler fails during discovery, the instance stays disconnected and method " + "calls return kCommunicationError."); + + EXPECT_CALL(mock_, SetReceiveHandler(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + + auto sut = MakeConnected(); + + auto get = sut->get_active_run_target(); + ASSERT_FALSE(get.has_value()); + EXPECT_EQ(get.error(), ExecErrc::kCommunicationError); +} + TEST_F(LmControlUT, ProxyCreateFailureLeavesInstanceDisconnected) { RecordProperty( From c812173947bcd8b58926268fd07c51aaa4b61d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 13:25:37 +0200 Subject: [PATCH 24/27] Fix bazel dependencies --- MODULE.bazel | 13 +------------ MODULE.bazel.lock | 5 +++++ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 6f82231a1..69bf48367 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -176,18 +176,7 @@ oci.pull( use_repo(oci, "debian-test-runtime", "debian-test-runtime_linux_amd64") bazel_dep(name = "score_baselibs", version = "0.2.10") -git_override( - module_name = "score_baselibs", - commit = "2daf5b88111d8f6e7f5516f240d4eacc67ceb361", - remote = "https://github.com/eclipse-score/baselibs.git", -) - -bazel_dep(name = "score_communication") -git_override( - module_name = "score_communication", - commit = "bcafda8fff9a90bc9e32bf054ad94fb7a627b318", - remote = "https://github.com/eclipse-score/communication.git", -) +bazel_dep(name = "score_communication", version = "0.3.0") # Hedron's Compile Commands Extractor for Bazel # https://github.com/hedronvision/bazel-compile-commands-extractor diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 65d45ecb7..83dd50858 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1014,6 +1014,9 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_swift/1.16.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_swift/1.18.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_swift/2.1.1/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_baselibs/0.2.10/MODULE.bazel": "eef4a41f8f8b6d5cf55c1445b340a2e8643a8dc83ad9ff52d0d26fe9a77973ec", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_baselibs/0.2.10/source.json": "543286fb4828ff58404a0ddd034cac3e7937657353997119a3976dbaf913f2cf", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_baselibs/0.2.9/MODULE.bazel": "2f95cc2e07c5ed9df69e6af35a98843235ce43a96d0aef8c55398ebe6813b58a", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_cpp_toolchains/0.5.4/MODULE.bazel": "5f14457f9b708302db05b5dc3c07bd41ae19b91cabf98a0dbffdca622bd6ac91", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_cpp_toolchains/0.5.4/source.json": "2946be24c6b978aa788c934643aa7ec68aac4a926bea4eaa81d512fd48e9f4a8", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/0.0.3/MODULE.bazel": "14c96e378c08705a46abe0799d6236fe3095c342c34f83f8d1b3f6046ce00651", @@ -1021,6 +1024,8 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/0.1.2/MODULE.bazel": "d1889bf36241521c5d5c401aaf1e98242a60f9cab9d223f8190e5d7087956e83", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/1.0.0/MODULE.bazel": "e5e386e6fc0a447a2f4a47e872e3747822e9d58994b7b55e248c78e5a3e382c0", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/1.0.0/source.json": "fb428e5f4cb6cbf0e00442e72c8d038746e0eaa7493dcec394ee99be727dcb3b", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_communication/0.3.0/MODULE.bazel": "7674d445cb51ea31e7ea69bc5c98f9dd6b81beeced915cb9c4e21b99debb61e0", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_communication/0.3.0/source.json": "d84f2add08dcbb2979078b3b7ffe8e10eb7c723bb8584ce74dd8476e98a75784", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_cpp_policies/0.0.1/MODULE.bazel": "e9d8ecb12ebd8d1fb2af6cfc3ba136600953d951983ef1e710f2f589de69a8c6", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_cpp_policies/0.0.1/source.json": "75f76a0d1a1208a94cbb43fc6455fc943ac262f354cfbb245da4220447d4c7f6", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_cr_checker/0.2.2/MODULE.bazel": "dc36d9c35543db918c3fb5b93a8e684431f56c7c784cf2a1b90f35802a373c98", From adfbdfd19b8fe2032503bf7964b5fa3c145c02b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 17 Aug 2026 14:02:32 +0200 Subject: [PATCH 25/27] Fix qnx tests --- score/launch_manager/src/lm_control/BUILD | 27 +++++++---- .../lm_control/src/details/ilm_control_UT.cpp | 8 ---- tests/utils/bazel/runfiles.bzl | 45 +++++++++++++++++++ 3 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 tests/utils/bazel/runfiles.bzl diff --git a/score/launch_manager/src/lm_control/BUILD b/score/launch_manager/src/lm_control/BUILD index f3c0c7bee..ca071b3c8 100644 --- a/score/launch_manager/src/lm_control/BUILD +++ b/score/launch_manager/src/lm_control/BUILD @@ -10,7 +10,9 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") +load("@rules_cc//cc:defs.bzl", "cc_library") +load("//tests/utils/bazel:runfiles.bzl", "runfiles_at") +load("//tests/utils/bazel:unit_test.bzl", "lm_cc_test") cc_library( name = "lm_control", @@ -38,7 +40,7 @@ cc_library( ], ) -cc_test( +lm_cc_test( name = "lm_control_impl_UT", srcs = ["src/details/lm_control_impl_UT.cpp"], deps = [ @@ -50,17 +52,24 @@ cc_test( ], ) -cc_test( +# mw::com reads its manifest from the working-directory-relative default path +# ./etc/mw_com_config.json unless told otherwise via CLI args. +# CLI args are currently not supported with score_qnx_unit_tests, so +# making sure the file is available at the default path in the runfiles of the test. +runfiles_at( + name = "default_mw_com_config", + testonly = True, + src = "src/details/test_lm_mw_com_config.json", + path = "etc/mw_com_config.json", +) + +lm_cc_test( name = "ilm_control_UT", srcs = ["src/details/ilm_control_UT.cpp"], - args = [ - "--service_instance_manifest", - "$(location src/details/test_lm_mw_com_config.json)", - ], - data = ["src/details/test_lm_mw_com_config.json"], + data = [":default_mw_com_config"], deps = [ ":lm_control", - "@googletest//:gtest", + "@googletest//:gtest_main", "@score_communication//score/mw/com", ], ) diff --git a/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp b/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp index 021ce56cc..c76e05d18 100644 --- a/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp +++ b/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp @@ -65,11 +65,3 @@ TEST_F(ILmControlUT, ReturnsInitErrorForInvalidSpecifier) } // namespace } // namespace score::mw::lifecycle - -/// Usage: ilm_control_UT --service_instance_manifest -int main(int argc, char** argv) -{ - ::testing::InitGoogleTest(&argc, argv); - score::mw::com::runtime::InitializeRuntime(argc, const_cast(argv)); - return RUN_ALL_TESTS(); -} diff --git a/tests/utils/bazel/runfiles.bzl b/tests/utils/bazel/runfiles.bzl new file mode 100644 index 000000000..8f31853df --- /dev/null +++ b/tests/utils/bazel/runfiles.bzl @@ -0,0 +1,45 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Bazel rules for placing files at a chosen location in a target's runfiles.""" + +def _runfiles_at_impl(ctx): + src = ctx.file.src + return DefaultInfo( + files = depset([src]), + runfiles = ctx.runfiles( + files = [src], + # root_symlinks are relative to the runfiles root, one level above the + # main repository directory - hence the explicit "_main/" prefix, which + # makes the path relative to the working directory of the executable. + root_symlinks = {"_main/" + ctx.attr.path: src}, + ), + ) + +runfiles_at = rule( + implementation = _runfiles_at_impl, + doc = """Exposes a single file at a fixed path in the runfiles of every target depending on it. + +Use it for files that a library expects at a hard-coded, working-directory-relative +location and that therefore cannot be passed as a command line argument.""", + attrs = { + "path": attr.string( + mandatory = True, + doc = "Destination path, relative to the working directory of the executable.", + ), + "src": attr.label( + allow_single_file = True, + mandatory = True, + doc = "The file to expose.", + ), + }, +) From f335a0581abc705f2f249b527a305dbb4cfef2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 18 Aug 2026 09:41:37 +0200 Subject: [PATCH 26/27] Cleanup --- score/launch_manager/src/lm_control/BUILD | 1 + .../src/details/lm_control_impl.hpp | 28 +++++++++----- .../src/details/lm_control_service.h | 38 +++++++------------ .../src/run_target_activation_source.hpp | 4 +- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/score/launch_manager/src/lm_control/BUILD b/score/launch_manager/src/lm_control/BUILD index ca071b3c8..e9d7151a2 100644 --- a/score/launch_manager/src/lm_control/BUILD +++ b/score/launch_manager/src/lm_control/BUILD @@ -35,6 +35,7 @@ cc_library( deps = [ "//score/launch_manager:error", "//score/launch_manager/src/daemon/src/common:log", + "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/result", "@score_communication//score/mw/com", ], diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp index 5f677e756..37147e4fd 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -24,6 +24,8 @@ #include "score/mw/lifecycle/details/lm_control_service.h" #include "score/mw/lifecycle/ilm_control.hpp" +#include + namespace score::mw::lifecycle { @@ -96,6 +98,8 @@ class BasicLmControlImpl final : public ILmControl /// @error kCommunicationError the service discovery could not be started. score::Result init(std::string_view instance_specifier) noexcept { + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE(!find_handle_.has_value(), "LmControl::init() called more than once"); + auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); if (!specifier_result.has_value()) { @@ -200,7 +204,8 @@ class BasicLmControlImpl final : public ILmControl const auto& response = *result.value(); if (response.status == QueryStatus::kNotAvailable) { - LM_LOG_DEBUG() << "LmControl: get_active_run_target: LM reports kActivationInProgress"; + LM_LOG_DEBUG() << "LmControl: get_active_run_target: Status not available, reason:" + << response.rejection_reason; return score::MakeUnexpected(response.rejection_reason); } @@ -242,10 +247,12 @@ class BasicLmControlImpl final : public ILmControl } /// @brief Create the proxy for the discovered instance and publish it into proxy_. - /// @pre Caller holds mutex_. + /// @pre Caller holds mutex_ and proxy_ is empty. /// @return true if the proxy was created, false on error (proxy_ left empty). bool createProxy(const HandleType& handle) noexcept { + SCORE_LANGUAGE_FUTURECPP_ASSERT_DBG_MESSAGE(!proxy_.has_value(), "proxy already created"); + auto create_result = Traits::Create(handle); if (!create_result.has_value()) { @@ -253,7 +260,7 @@ class BasicLmControlImpl final : public ILmControl return false; } proxy_.emplace(std::move(create_result).value()); - LM_LOG_INFO() << "LmControl: proxy created successfully"; + LM_LOG_DEBUG() << "LmControl: proxy created successfully"; return true; } @@ -262,6 +269,8 @@ class BasicLmControlImpl final : public ILmControl /// @return true on success; on failure proxy_ is reset so we stay unconnected. bool subscribeToActivationResults() noexcept { + SCORE_LANGUAGE_FUTURECPP_ASSERT_DBG_MESSAGE(proxy_.has_value(), "proxy must not be null"); + const auto subscribe_result = proxy_->activation_result.Subscribe(kActivationResultSampleCount); if (!subscribe_result.has_value()) { @@ -297,12 +306,11 @@ class BasicLmControlImpl final : public ILmControl } /// @brief Ready and forwards all pending activation-result samples. - /// @details The callback is copied once here rather than once per sample: a registration - /// racing with a running drain takes effect from the next handler invocation on, - /// never mid-drain. - /// @pre The caller holds sample_processing_mutex_. + /// @pre The caller holds sample_processing_mutex_ and proxy_ has a value. void readActivationEvents() { + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE(proxy_.has_value(), "proxy must not be null"); + const ActivationCallback callback = currentCallback(); if (!callback) { @@ -364,12 +372,12 @@ class BasicLmControlImpl final : public ILmControl } } - // Guards proxy_ and the service discovery handle. + // Guards proxy_ std::mutex mutex_; std::optional proxy_; - // Handle of the running discovery search, stopped by the destructor. Written once by the - // constructor and never touched again, so it needs no lock. + // Handle of the running discovery search, stopped by the destructor. Written once by init() + // and never touched again, so it needs no lock. std::optional find_handle_; ActivationCallback callback_; diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_service.h b/score/launch_manager/src/lm_control/src/details/lm_control_service.h index e760115da..b4797651b 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_service.h +++ b/score/launch_manager/src/lm_control/src/details/lm_control_service.h @@ -28,8 +28,7 @@ namespace score::mw::lifecycle /// @brief Controls how an activation request interacts with any in-progress activation. /// -/// Mirrors the `force` parameter of `ILmControl::activate_run_target`. The -/// underlying storage is `uint8_t` to keep the wire representation compact. +/// @details Mirrors the `force` parameter of `ILmControl::activate_run_target`. enum class ActivationMode : uint8_t { kQueued = 0, ///< Queue this request behind any in-progress activation. @@ -44,7 +43,7 @@ struct ActivateRunTargetRequest RunTargetName run_target_name; /// @brief Whether to queue or force this activation. - ActivationMode mode; + ActivationMode mode{ActivationMode::kQueued}; }; /// @brief Indicates whether the request was accepted or rejected by the Launch Manager. @@ -61,10 +60,10 @@ enum class RequestStatus : uint8_t struct ActivateRunTargetResponse { /// @brief Whether the Launch Manager accepted or rejected the request. - RequestStatus status; + RequestStatus status{RequestStatus::kRejected}; /// @brief Reason for rejection. Only meaningful when status == RequestStatus::kRejected. - ExecErrc rejection_reason; + ExecErrc rejection_reason{ExecErrc::kGeneralError}; }; // ============================================================================ @@ -74,15 +73,15 @@ struct ActivateRunTargetResponse /// @brief Indicates whether the Launch Manager can provide the currently active Run Target. enum class QueryStatus : uint8_t { - kAvailable = 0, ///< A settled Run Target exists; see the run_target field. - kNotAvailable = 1, ///< No settled Run Target; an activation is in progress. + kAvailable = 0, ///< An active Run Target exists; see the run_target field. + kNotAvailable = 1, ///< No active Run Target; an activation is in progress. }; /// @brief Synchronous response to a get_active_run_target query. struct GetActiveRunTargetResponse { - /// @brief Whether a settled Run Target is currently available. - QueryStatus status; + /// @brief Whether an active Run Target is currently available. + QueryStatus status{QueryStatus::kNotAvailable}; /// @brief The currently active Run Target. /// Only meaningful when status == QueryStatus::kAvailable. @@ -91,26 +90,25 @@ struct GetActiveRunTargetResponse /// @brief Reason the active Run Target is not available. /// Only meaningful when status == QueryStatus::kNotAvailable. /// Expected value: kActivationInProgress. - ExecErrc rejection_reason; + ExecErrc rejection_reason{ExecErrc::kGeneralError}; }; // ============================================================================ // activation_result event // ============================================================================ -/// @brief Event sent by the Launch Manager when a Run Target activation settles. +/// @brief Event sent by the Launch Manager when a Run Target activation completes. /// -/// Sent to all subscribers regardless of which State Manager originated the request. /// Activation always resolves into some Run Target — it cannot fail at the graph -/// level. The settled Run Target may differ from the one originally requested if a +/// level. The activated Run Target may differ from the one originally requested if a /// preempting request or recovery action redirected the transition. struct ActivationResult { - /// @brief The Run Target the Launch Manager settled on. + /// @brief The Run Target the Launch Manager activated. RunTargetName activated_run_target; /// @brief What caused this activation to occur. - RunTargetActivationSource activation_source; + RunTargetActivationSource activation_source{RunTargetActivationSource::kStateManagerRequest}; }; // ============================================================================ @@ -118,11 +116,6 @@ struct ActivationResult // ============================================================================ /// @brief mw::com service definition for the Launch Manager control interface. -/// -/// The State Manager holds a `LmControlProxy` and calls `activate_run_target` -/// and `get_active_run_target`. The Launch Manager holds a `LmControlSkeleton`, -/// registers handlers for those methods, and fires `activation_result` events -/// on Run Target settling. template class LmControlService : public Trait::Base { @@ -147,10 +140,7 @@ class LmControlService : public Trait::Base /// activation_result event and retry if needed. typename Trait::template Method get_active_run_target{*this, "GetActiveRunTarget"}; - /// @brief Event fired by the Launch Manager when a Run Target activation settles. - /// - /// All connected proxies receive this event. Each subscriber's registered - /// ActivationCallback is invoked with the settled Run Target and source. + /// @brief Event fired by the Launch Manager when a Run Target activation completes. typename Trait::template Event activation_result{*this, "ActivationResult"}; }; diff --git a/score/launch_manager/src/lm_control/src/run_target_activation_source.hpp b/score/launch_manager/src/lm_control/src/run_target_activation_source.hpp index 7204c930e..36a60d389 100644 --- a/score/launch_manager/src/lm_control/src/run_target_activation_source.hpp +++ b/score/launch_manager/src/lm_control/src/run_target_activation_source.hpp @@ -14,6 +14,8 @@ #ifndef SCORE_MW_LIFECYCLE_RUN_TARGET_ACTIVATION_SOURCE_HPP #define SCORE_MW_LIFECYCLE_RUN_TARGET_ACTIVATION_SOURCE_HPP +#include + namespace score::mw::lifecycle { @@ -25,7 +27,7 @@ namespace score::mw::lifecycle /// @note Activation cannot fail and always resolves into some /// Run Target. This enum describes *why* it resolved, /// not whether it succeeded. -enum class RunTargetActivationSource +enum class RunTargetActivationSource : std::uint8_t { /// @brief Activation was explicitly requested by a State Manager. kStateManagerRequest = 0, From 98e9a28c8d66254825453183e4c7a4bc0b336322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 18 Aug 2026 10:02:19 +0200 Subject: [PATCH 27/27] Fix test description --- .../src/details/lm_control_impl_UT.cpp | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp index 8923c4d89..1cc66cd68 100644 --- a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp @@ -349,24 +349,6 @@ TEST_F(LmControlUT, DiscoveryCreatesProxySubscribesAndSetsHandler) EXPECT_TRUE(receive_handler_); } -TEST_F(LmControlUT, DiscoveryKeepsRunningAfterConnect) -{ - RecordProperty( - "Description", - "Connecting does not stop the discovery search; it keeps running until the instance is destroyed."); - - EXPECT_CALL(mock_, StopFindService()).Times(0); - - auto sut = MakeConnected(); - // A further notification while already connected must not stop discovery either. - ConnectService(); - EXPECT_TRUE(sut->get_active_run_target().has_value()); - - // Verify while the instance is still alive - the destructor is what stops discovery, and it - // is covered by DestructorStopsDiscoveryWhenNeverConnected / DestructorUnsubscribesWhenConnected. - ::testing::Mock::VerifyAndClearExpectations(&mock_); -} - TEST_F(LmControlUT, EmptyHandlesAreIgnored) { RecordProperty( @@ -397,15 +379,15 @@ TEST_F(LmControlUT, SynchronousDiscoveryDuringConstructionStopsFindingOnce) { RecordProperty( "Description", - "When discovery completes synchronously during construction, discovery is stopped exactly once and " - "not again in the destructor."); + "When discovery completes synchronously while init() is still running, the service discovery is still " + "stopped exactly once, in the destructor."); EXPECT_CALL(mock_, StartFindService(_)).WillOnce(Invoke([](FindHandler handler) { handler(score::mw::com::ServiceHandleContainer{FakeHandle{}}, FakeFindHandle{}); return score::Result{FakeFindHandle{}}; })); - // Discovery is stopped exactly once, by the destructor. The synchronous handler leaves the - // search untouched, so the constructor can store the handle unconditionally. + // The destructor is the only place that stops discovery. The synchronous handler leaves the + // search untouched, so init() can store the handle unconditionally. EXPECT_CALL(mock_, StopFindService()).Times(1); auto sut = MakeLmControl(); @@ -630,8 +612,8 @@ TEST_F(LmControlUT, ActivationResultBacklogExceedingSubscriptionIsDrainedInSever { RecordProperty( "Description", - "A backlog larger than the subscribed max_sample_count is drained with repeated GetNewSamples calls " - "until a batch smaller than the maximum signals an empty queue; every sample is forwarded exactly once."); + "Number of available samples larger than subscribed max_sample_count is read with repeated GetNewSamples " + "calls until a batch smaller than the maximum signals an empty queue; every sample is forwarded exactly once."); auto sut = MakeConnected(); @@ -696,7 +678,7 @@ TEST_F(LmControlUT, ActivationResultWithoutCallbackIsDroppedSafely) TEST_F(LmControlUT, ActivationResultGetNewSamplesFailureIsTolerated) { RecordProperty( - "Description", "A GetNewSamples failure while draining activation results is tolerated without crashing."); + "Description", "A GetNewSamples failure while reading activation results is tolerated without crashing."); auto sut = MakeConnected();