From 2ca10e9ec3e02b94cf5e438acf196f63aa6301b5 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Mon, 3 Aug 2026 08:40:02 +0100 Subject: [PATCH 1/2] Adding new config field --- .../docs/user_guide/configuration.rst | 15 +++ .../src/daemon/src/configuration/config.hpp | 16 ++- .../config_schema/launch_manager.schema.json | 30 ++++- .../configuration/configuration_adapter.cpp | 43 ++++--- .../configuration/configuration_adapter.hpp | 9 +- .../configuration_adapter_UT.cpp | 112 ++++++++++++++++++ .../details/flatbuffer_config_loader_UT.cpp | 49 +++++++- .../details/flatbuffer_type_converters.cpp | 73 +++++++++--- .../details/flatbuffer_type_converters.hpp | 7 +- .../details/flatbuffer_type_converters_UT.cpp | 95 ++++++++++++++- .../src/configuration/details/lm_flatcfg.fbs | 22 +++- 11 files changed, 428 insertions(+), 43 deletions(-) diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index a51caa11d..1678691a9 100644 --- a/score/launch_manager/docs/user_guide/configuration.rst +++ b/score/launch_manager/docs/user_guide/configuration.rst @@ -208,6 +208,21 @@ component_properties (object) * **Allowed Values:** * ``"Running"``: The process has started and reached its running state. * ``"Terminated"``: The process has started, reached its running state, and then terminated successfully. + * **file_state** (object, optional) + * **Description:** Specifies a ready condition based on the existence state of a file at a given path. + * **Properties:** + * **file_path** (string, required) + * **Description:** Specifies the absolute path to the file being watched. + * **state** (string, optional) + * **Description:** Specifies the required existence state of the file. + * **Allowed Values:** + * ``"Exists"``: The component is ready when the file at ``file_path`` exists. + * ``"Deleted"``: The component is ready when the file at ``file_path`` is deleted. + * **Default:** ``"Exists"`` + * **polling_interval** (integer, optional) + * **Description:** Specifies the time interval, in milliseconds, at which the **Launch Manager** checks the file existence state. + * **Constraint:** Must be greater than 0. + * **Default:** ``10`` .. _lm_conf_deployment_config_object_: diff --git a/score/launch_manager/src/daemon/src/configuration/config.hpp b/score/launch_manager/src/daemon/src/configuration/config.hpp index 12dbfdbd7..b4a2c09d0 100644 --- a/score/launch_manager/src/daemon/src/configuration/config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/config.hpp @@ -14,10 +14,12 @@ #define CONFIG_HPP #include +#include #include #include #include #include +#include #include namespace score::mw::lifecycle::internal::configuration @@ -37,6 +39,12 @@ enum class ProcessState : uint8_t Terminated = 1 }; +enum class FileExistenceState : uint8_t +{ + Exists = 0, + Deleted, +}; + struct ComponentAliveSupervision { uint32_t reporting_cycle_ms{}; @@ -52,11 +60,15 @@ struct ApplicationProfile std::optional alive_supervision; }; -struct ReadyCondition +struct FileState { - ProcessState process_state{ProcessState::Running}; + std::string file_path; + FileExistenceState state{FileExistenceState::Exists}; + std::chrono::milliseconds polling_interval{10}; }; +using ReadyCondition = std::variant; + struct ComponentProperties { std::string binary_name; diff --git a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json index 371630d73..990a9411d 100644 --- a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json +++ b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json @@ -89,6 +89,34 @@ "Terminated" ], "description": "Specifies the required state of the component's POSIX process. 'Running': the process has started and reached its running state. 'Terminated': the process has started, reached its running state, and then terminated successfully." + }, + "file_state": { + "type": "object", + "description": "Specifies a ready condition based on the existence state of a file at a given path.", + "properties": { + "file_path": { + "type": "string", + "pattern": "^/.*", + "description": "Specifies the absolute path to the file being watched." + }, + "state": { + "type": "string", + "enum": [ + "Exists", + "Deleted" + ], + "description": "Specifies the required existence state of the file. 'Exists': the file must be present at 'file_path'. 'Deleted': the file must be absent from 'file_path'. Defaults to 'Exists' if not specified." + }, + "polling_interval": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the time interval, in milliseconds, at which the Launch Manager checks the file existence state." + } + }, + "required": [ + "file_path" + ], + "additionalProperties": false } }, "required": [], @@ -488,4 +516,4 @@ "initial_run_target" ], "additionalProperties": false -} \ No newline at end of file +} diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp index 20564194b..6688e4698 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -30,7 +31,7 @@ namespace constexpr const char* kAliveInterfaceEnvName = "LCM_ALIVE_INTERFACE_PATH"; constexpr uint32_t kDefaultProcessExecutionError = 1U; -uint64_t defaultProcessorAffinityMask() +[[maybe_unused]] uint64_t defaultProcessorAffinityMask() { return (1ULL << score::mw::lifecycle::internal::osal::getNumCores()) - 1ULL; } @@ -202,19 +203,34 @@ DependencyList ConfigurationAdapter::buildDependencyList(const ComponentProperti for (const auto& dep_name : props.depends_on) { + auto dep_it = component_by_name_.find(dep_name); + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + dep_it != component_by_name_.end(), "Component's dependency points to a non-existent component"); + + const auto& dep_props = dep_it->second->component_properties; + Dependency dep{}; dep.process_state_ = score::mw::lifecycle::ProcessState::kRunning; - - auto dep_it = component_by_name_.find(dep_name); - if (dep_it != component_by_name_.end()) + if (dep_props.ready_condition.has_value()) { - const auto& dep_props = dep_it->second->component_properties; - if (dep_props.ready_condition.has_value()) - { - dep.process_state_ = dep_props.ready_condition->process_state == ProcessState::Running - ? score::mw::lifecycle::ProcessState::kRunning - : score::mw::lifecycle::ProcessState::kTerminated; - } + std::visit( + [&dep](auto&& arg) { + using argT = std::decay_t; + + if constexpr (std::is_same_v) + { + dep.process_state_ = arg == ProcessState::Running + ? score::mw::lifecycle::ProcessState::kRunning + : score::mw::lifecycle::ProcessState::kTerminated; + return; + } + else if constexpr (std::is_same_v) + { + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE("FileState is not yet supported"); + return; + } + }, + dep_props.ready_condition.value()); } dep.target_process_id_ = IdentifierHash{dep_name}; @@ -255,11 +271,9 @@ void ConfigurationAdapter::resolveDependsOnEntry( return; } - bool found = false; auto comp_it = component_to_process_index_.find(dep_name); if (comp_it != component_to_process_index_.end()) { - found = true; if (std::find(indexes.begin(), indexes.end(), comp_it->second) == indexes.end()) { indexes.push_back(comp_it->second); @@ -278,14 +292,11 @@ void ConfigurationAdapter::resolveDependsOnEntry( auto dep_it = depends_on_by_name.find(dep_name); if (dep_it != depends_on_by_name.end()) { - found = true; for (const auto& sub_dep : *dep_it->second) { resolveDependsOnEntry(sub_dep, depends_on_by_name, indexes, visited); } } - - assert(found && "depends_on references unknown component or run_target"); } ProcessGroupState ConfigurationAdapter::buildProcessGroupState( diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp index 9cc87683d..3d6203cf1 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp @@ -107,21 +107,28 @@ class ConfigurationAdapter final bool buildFromConfig(const Config& config); OsProcess buildOsProcess(const ComponentConfig& comp, uint32_t process_index) const; + void fillStartupConfigFromDeployment( const ComponentConfig& comp, score::mw::lifecycle::internal::osal::OsalConfig& startup) const; + void fillStartupArguments( const ComponentProperties& props, score::mw::lifecycle::internal::osal::OsalConfig& startup) const; + size_t fillStartupEnvironment( const DeploymentConfig& deploy, score::mw::lifecycle::internal::osal::OsalConfig& startup) const; + void appendAliveInterfaceEnvironment( const ComponentConfig& comp, size_t& env_index, score::mw::lifecycle::internal::osal::OsalConfig& startup) const; + PgManagerConfig buildPgManagerConfig(const ComponentConfig& comp) const; - DependencyList buildDependencyList(const ComponentProperties& props) const; + + /// @brief Given a components properties, creates a list of dependencies. + [[nodiscard]] DependencyList buildDependencyList(const ComponentProperties& props) const; std::vector buildProcessGroupStates(const Config& config) const; ProcessGroupState buildProcessGroupState( diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp index d07d4daae..b12a52ffc 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp @@ -571,5 +571,117 @@ TEST(ConfigurationAdapterFallbackTest, FallbackRunTargetResolvesDependenciesRecu adapter.deinitialize(); } +TEST(ConfigurationAdapterDependencyTest, DependencyOnNonExistentComponentIsIgnored) +{ + RecordProperty("Description", "When a component depends on a non-existent component, the dependency is skipped."); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "explorative-testing"); + + ComponentConfig comp_a; + comp_a.name = "comp_a"; + comp_a.component_properties.application_profile.application_type = ApplicationType::Native; + comp_a.component_properties.application_profile.is_self_terminating = false; + comp_a.component_properties.depends_on = {"non_existent_component", "also_missing"}; + comp_a.deployment_config.bin_dir = "/opt"; + comp_a.component_properties.binary_name = "comp_a"; + comp_a.deployment_config.working_dir = "/tmp"; + comp_a.deployment_config.sandbox.uid = 0; + comp_a.deployment_config.sandbox.gid = 0; + comp_a.deployment_config.sandbox.scheduling_policy = SCHED_OTHER; + comp_a.deployment_config.sandbox.scheduling_priority = 0; + + std::vector components; + components.push_back(std::move(comp_a)); + + RunTargetConfig startup; + startup.name = "Startup"; + startup.depends_on = {"comp_a"}; + startup.transition_timeout_ms = 5000; + startup.recovery_action.run_target = "fallback_run_target"; + + std::vector run_targets; + run_targets.push_back(std::move(startup)); + + FallbackRunTargetConfig fallback; + fallback.transition_timeout_ms = 1500; + AliveSupervisionConfig alive; + alive.evaluation_cycle_ms = 500; + + auto config = ConfigBuilder{} + .setComponents(std::move(components)) + .setRunTargets(std::move(run_targets)) + .setInitialRunTarget("Startup") + .setFallbackRunTarget(std::move(fallback)) + .setAliveSupervision(alive) + .build(); + + ConfigurationAdapter adapter; + EXPECT_DEATH(adapter.initialize(config), "Component's dependency.*"); +} + +TEST(ConfigurationAdapterReadyConditionTest, FileStateReadyConditionTriggersAssert) +{ + RecordProperty("Description", "When a dependency target has FileState ready_condition, it triggers an assertion."); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "explorative-testing"); + + ComponentConfig comp_a; + comp_a.name = "comp_a"; + comp_a.component_properties.application_profile.application_type = ApplicationType::Native; + comp_a.component_properties.application_profile.is_self_terminating = false; + FileState file_state{"/tmp/ready.txt", FileExistenceState::Exists, std::chrono::milliseconds{100}}; + comp_a.component_properties.ready_condition = ReadyCondition{file_state}; + comp_a.deployment_config.bin_dir = "/opt"; + comp_a.component_properties.binary_name = "comp_a"; + comp_a.deployment_config.working_dir = "/tmp"; + comp_a.deployment_config.sandbox.uid = 0; + comp_a.deployment_config.sandbox.gid = 0; + comp_a.deployment_config.sandbox.scheduling_policy = SCHED_OTHER; + comp_a.deployment_config.sandbox.scheduling_priority = 0; + + ComponentConfig comp_b; + comp_b.name = "comp_b"; + comp_b.component_properties.application_profile.application_type = ApplicationType::Native; + comp_b.component_properties.application_profile.is_self_terminating = false; + comp_b.component_properties.ready_condition = ReadyCondition{ProcessState::Running}; + comp_b.component_properties.depends_on = {"comp_a"}; + comp_b.deployment_config.bin_dir = "/opt"; + comp_b.component_properties.binary_name = "comp_b"; + comp_b.deployment_config.working_dir = "/tmp"; + comp_b.deployment_config.sandbox.uid = 0; + comp_b.deployment_config.sandbox.gid = 0; + comp_b.deployment_config.sandbox.scheduling_policy = SCHED_OTHER; + comp_b.deployment_config.sandbox.scheduling_priority = 0; + + std::vector components; + components.push_back(std::move(comp_a)); + components.push_back(std::move(comp_b)); + + RunTargetConfig startup; + startup.name = "Startup"; + startup.depends_on = {"comp_b"}; + startup.transition_timeout_ms = 5000; + startup.recovery_action.run_target = "fallback_run_target"; + + std::vector run_targets; + run_targets.push_back(std::move(startup)); + + FallbackRunTargetConfig fallback; + fallback.transition_timeout_ms = 1500; + AliveSupervisionConfig alive; + alive.evaluation_cycle_ms = 500; + + auto config = ConfigBuilder{} + .setComponents(std::move(components)) + .setRunTargets(std::move(run_targets)) + .setInitialRunTarget("Startup") + .setFallbackRunTarget(std::move(fallback)) + .setAliveSupervision(alive) + .build(); + + ConfigurationAdapter adapter; + EXPECT_DEATH(adapter.initialize(config), "FileState.*"); +} + } // namespace } // namespace score::mw::lifecycle::internal::configuration diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp index 85c4e1b3c..63398ae09 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp @@ -31,10 +31,12 @@ namespace namespace fb = score::mw::lifecycle::internal::configuration::fb; using ::testing::Eq; +using ::testing::FieldsAre; using ::testing::IsFalse; using ::testing::IsNull; using ::testing::IsTrue; using ::testing::StrEq; +using ::testing::VariantWith; const score::filesystem::Path kTestPath{"/tmp/test_config.bin"}; @@ -258,13 +260,58 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponent) ASSERT_THAT(comp.component_properties.process_arguments.size(), Eq(1U)); EXPECT_THAT(comp.component_properties.process_arguments[0], Eq("--verbose")); ASSERT_THAT(comp.component_properties.ready_condition.has_value(), IsTrue()); - EXPECT_THAT(comp.component_properties.ready_condition->process_state, Eq(ProcessState::Running)); + EXPECT_THAT(*comp.component_properties.ready_condition, VariantWith(Eq(ProcessState::Running))); EXPECT_THAT(comp.deployment_config.ready_timeout_ms, Eq(1500U)); EXPECT_THAT(comp.deployment_config.shutdown_timeout_ms, Eq(2500U)); EXPECT_THAT(comp.deployment_config.bin_dir, Eq("/opt/bin")); EXPECT_THAT(comp.deployment_config.working_dir, Eq("/tmp")); } +TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponentWithFileState) +{ + RecordProperty("Description", "Loads a component whose ready_condition includes a file_state."); + + ::flatbuffers::FlatBufferBuilder fbb; + + auto app_profile = fb::CreateApplicationProfile(fbb, fb::ApplicationType::Native, false /*is_self_terminating*/); + auto bin_name = fbb.CreateString("my_binary"); + auto file_state = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); + auto ready_cond = fb::CreateReadyCondition(fbb, std::nullopt, file_state); + auto comp_props = fb::CreateComponentProperties( + fbb, bin_name, app_profile, 0 /*depends_on*/, 0 /*process_arguments*/, ready_cond); + + auto bin_dir = fbb.CreateString("/opt/bin"); + auto work_dir = fbb.CreateString("/tmp"); + auto sandbox = buildDefaultSandbox(fbb); + auto deploy = fb::CreateDeploymentConfig( + fbb, + 1.5 /*ready_timeout*/, + 2.5 /*shutdown_timeout*/, + 0 /*environmental_variables*/, + bin_dir, + work_dir, + 0 /*ready_recovery_action*/, + 0 /*recovery_action*/, + sandbox); + + auto comp_name = fbb.CreateString("TestComponent"); + auto comp_desc = fbb.CreateString("A test component"); + auto component = fb::CreateComponent(fbb, comp_name, comp_desc, comp_props, deploy); + auto comps = fbb.CreateVector(std::vector<::flatbuffers::Offset>{component}); + + auto result = loadBuffer(buildConfigWithComponents(fbb, comps)); + + ASSERT_THAT(result.has_value(), IsTrue()); + ASSERT_THAT(result->components().size(), Eq(1U)); + + const auto& comp = result->components()[0]; + ASSERT_THAT(comp.component_properties.ready_condition.has_value(), IsTrue()); + EXPECT_THAT( + *comp.component_properties.ready_condition, + VariantWith( + FieldsAre(Eq("/tmp/ready"), Eq(FileExistenceState::Exists), Eq(std::chrono::milliseconds{10})))); +} + TEST_F(FlatbufferConfigLoaderTest, LoadRunTargets) { RecordProperty("Description", "Loads run targets with dependencies and transition timeout."); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 6c5c4f0fd..20e0bdfdd 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -108,6 +108,18 @@ ProcessState convertProcessState(fb::ProcessState fb_state) } } +FileExistenceState convertFileExistenceState(fb::FileExistenceState fb_state) +{ + switch (fb_state) + { + case fb::FileExistenceState::Deleted: + return FileExistenceState::Deleted; + case fb::FileExistenceState::Exists: + return FileExistenceState::Exists; + } + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE(); +} + score::cpp::expected convertSchedulingPolicy(fb::SchedulingPolicy policy) { switch (policy) @@ -301,19 +313,57 @@ score::cpp::expected convertApplicatio return result; } -score::cpp::expected convertReadyCondition(const fb::ReadyCondition* fb_rc) +std::optional convertFileState(const fb::FileState* fb_fs) +{ + if (fb_fs == nullptr) + { + return std::nullopt; + } + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + fb_fs->file_path(), "FileState::file_path must never be nullptr as it is required in the schema"); + + return FileState{ + fb_fs->file_path()->str(), + convertFileExistenceState(fb_fs->state()), + std::chrono::milliseconds{fb_fs->polling_interval()}}; +} + +std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc) { - ReadyCondition result{}; - if (fb_rc != nullptr) + if (fb_rc == nullptr) + { + return std::nullopt; + } + + const bool has_process_state = fb_rc->process_state().has_value(); + const bool has_file_state = fb_rc->file_state() != nullptr; + + if (has_process_state && has_file_state) + { + LM_LOG_ERROR() << "ReadyCondition cannot have both process_state and file_state set"; + return std::nullopt; + } + + if (!has_process_state && !has_file_state) + { + LM_LOG_ERROR() << "ReadyCondition must have either process_state or file_state set"; + return std::nullopt; + } + + if (has_process_state) + { + return convertProcessState(*fb_rc->process_state()); + } + else { - auto process_state = requireScalarValue(fb_rc->process_state(), "ReadyCondition::process_state"); - if (!process_state.has_value()) + auto file_state = convertFileState(fb_rc->file_state()); + if (!file_state.has_value()) { - return score::cpp::make_unexpected(process_state.error()); + LM_LOG_ERROR() << "FileState conversion failed"; + return std::nullopt; } - result.process_state = convertProcessState(*process_state); + return *file_state; } - return result; } score::cpp::expected convertComponentProperties( @@ -339,12 +389,7 @@ score::cpp::expected convertComponent result.process_arguments = convertStringVector(fb_cp->process_arguments()); if (fb_cp->ready_condition() != nullptr) { - auto ready_cond = convertReadyCondition(fb_cp->ready_condition()); - if (!ready_cond.has_value()) - { - return score::cpp::make_unexpected(ready_cond.error()); - } - result.ready_condition = std::move(*ready_cond); + result.ready_condition = convertReadyCondition(fb_cp->ready_condition()); } } return result; diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp index e22431ec7..67d836c81 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp @@ -69,6 +69,10 @@ score::cpp::expected validateRange(int64_t value, [[nodiscard]] ApplicationType convertApplicationType(fb::ApplicationType fb_type); /// @brief Converts a FlatBuffer ProcessState enum to the config ProcessState. [[nodiscard]] ProcessState convertProcessState(fb::ProcessState fb_state); +/// @brief Converts a FlatBuffer FileState struct to the config equivalent. +std::optional convertFileState(const fb::FileState* fb_fs); +/// @brief Converts a FlatBuffer FileExistenceState enum to the config equivalent. +[[nodiscard]] FileExistenceState convertFileExistenceState(fb::FileExistenceState fb_state); /// @brief Converts a FlatBuffer SchedulingPolicy enum to a POSIX scheduling policy constant. [[nodiscard]] score::cpp::expected convertSchedulingPolicy(fb::SchedulingPolicy policy); @@ -105,8 +109,7 @@ score::cpp::expected validateRange(int64_t value, [[nodiscard]] score::cpp::expected convertApplicationProfile( const fb::ApplicationProfile* fb_ap); /// @brief Converts a FlatBuffer ReadyCondition to the config equivalent. -[[nodiscard]] score::cpp::expected convertReadyCondition( - const fb::ReadyCondition* fb_rc); +[[nodiscard]] std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc); /// @brief Converts a FlatBuffer ComponentProperties to the config equivalent. [[nodiscard]] score::cpp::expected convertComponentProperties( const fb::ComponentProperties* fb_cp); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index 0fb421246..b50914bf9 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -562,7 +562,14 @@ TEST_F(ConverterTest, ConvertApplicationProfileMissingSelfTerminatingReturnsErro EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } -TEST_F(ConverterTest, ConvertReadyConditionValid) +TEST_F(ConverterTest, ConvertReadyConditionNullReturnsNullopt) +{ + RecordProperty("Description", "convertReadyCondition with nullptr returns nullopt."); + auto result = details::convertReadyCondition(nullptr); + EXPECT_THAT(result.has_value(), IsFalse()); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithProcessState) { RecordProperty("Description", "convertReadyCondition maps process_state correctly."); ::flatbuffers::FlatBufferBuilder fbb; @@ -572,12 +579,40 @@ TEST_F(ConverterTest, ConvertReadyConditionValid) auto result = details::convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsTrue()); - EXPECT_THAT(result->process_state, Eq(ProcessState::Terminated)); + EXPECT_THAT(*result, ::testing::VariantWith(ProcessState::Terminated)); } -TEST_F(ConverterTest, ConvertReadyConditionMissingProcessStateReturnsError) +TEST_F(ConverterTest, ConvertReadyConditionWithFileState) { - RecordProperty("Description", "Missing process_state returns InvalidFormat."); + RecordProperty("Description", "convertReadyCondition maps file_state correctly."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); + auto rc = fb::CreateReadyCondition(fbb, ::flatbuffers::nullopt /*process_state*/, fs); + fbb.Finish(rc); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertReadyCondition(ptr); + ASSERT_THAT(result.has_value(), IsTrue()); + EXPECT_THAT(*result, ::testing::VariantWith(::testing::Field(&FileState::file_path, Eq("/tmp/ready")))); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithBothStatesReturnsNullopt) +{ + RecordProperty("Description", "convertReadyCondition with both process_state and file_state returns nullopt."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); + auto rc = fb::CreateReadyCondition(fbb, fb::ProcessState::Running, fs); + fbb.Finish(rc); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertReadyCondition(ptr); + ASSERT_THAT(result.has_value(), IsFalse()); + EXPECT_EQ(result, std::nullopt); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithNeitherStateReturnsNullopt) +{ + RecordProperty("Description", "convertReadyCondition with neither process_state nor file_state returns nullopt."); ::flatbuffers::FlatBufferBuilder fbb; auto rc = fb::CreateReadyCondition(fbb); fbb.Finish(rc); @@ -585,7 +620,57 @@ TEST_F(ConverterTest, ConvertReadyConditionMissingProcessStateReturnsError) auto result = details::convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsFalse()); - EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); + EXPECT_EQ(result, std::nullopt); +} + +TEST_F(ConverterTest, ConvertFileExistenceStateMapsDeath) +{ + RecordProperty("Description", "convertFileExistenceState Fires an assertion if an undefined enum is given."); + EXPECT_DEATH( + static_cast(details::convertFileExistenceState( + static_cast(static_cast(fb::FileExistenceState::MAX) + 1))), + ".*"); +} + +TEST_F(ConverterTest, ConvertFileExistenceStateMapsBothValues) +{ + RecordProperty("Description", "convertFileExistenceState maps both enum values correctly."); + EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Exists), Eq(FileExistenceState::Exists)); + EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Deleted), Eq(FileExistenceState::Deleted)); +} + +TEST_F(ConverterTest, ConvertFileStateNullReturnsNullopt) +{ + RecordProperty("Description", "convertFileState returns nullopt when passed nullptr."); + auto result = details::convertFileState(nullptr); + EXPECT_THAT(result.has_value(), IsFalse()); +} + +TEST_F(ConverterTest, ConvertFileStateValid) +{ + RecordProperty("Description", "convertFileState maps file_path and an explicit state correctly."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Deleted); + fbb.Finish(fs); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertFileState(ptr); + ASSERT_THAT(result.has_value(), IsTrue()); + EXPECT_THAT(result->file_path, Eq("/tmp/ready")); + EXPECT_THAT(result->state, Eq(FileExistenceState::Deleted)); +} + +TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) +{ + RecordProperty("Description", "convertFileState defaults state to Exists when not specified."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready"); + fbb.Finish(fs); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertFileState(ptr); + ASSERT_THAT(result.has_value(), IsTrue()); + EXPECT_THAT(result->state, Eq(FileExistenceState::Exists)); } TEST_F(ConverterTest, ConvertSandboxValid) diff --git a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs index 83529b05c..76cdf9229 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs @@ -26,6 +26,12 @@ enum ProcessState : byte { Terminated = 1 } +// Specifies the required existence state of a watched file. +enum FileExistenceState : byte { + Exists = 0, + Deleted = 1 +} + // Scheduling policy for a component's initial thread. enum SchedulingPolicy : byte { OTHER = 0, @@ -53,9 +59,23 @@ table ApplicationProfile { alive_supervision:ComponentAliveSupervision; // optional } +// Defines a ready condition based on the existence state of a file at a given path. +table FileState { + // Absolute path to the file being watched. + file_path:string (required); // required + // Existence state of the file. Defaults to Exists if not specified. + state:FileExistenceState = Exists; // optional, defaults to Exists + // Time in ms to wait between each poll if the file is present. + polling_interval: uint32 = 10; //optional, defaults to 10ms +} + // Defines the conditions that determine when the component enters the ready state. +// Either process_state or file_state should be set, but not both. table ReadyCondition { - process_state:ProcessState = null; // required + // Required state of the component's POSIX process. + process_state:ProcessState = null; // optional + // File existence state condition. + file_state:FileState; // optional } // Defines essential characteristics of a software component. From 7377dba5fbdf1eda807f6c919888c776a16d220a Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Thu, 13 Aug 2026 14:06:37 +0100 Subject: [PATCH 2/2] Using seconds --- .../docs/user_guide/configuration.rst | 10 ++++---- .../src/daemon/src/configuration/config.hpp | 2 +- .../config_schema/launch_manager.schema.json | 25 +++++++++++++------ .../details/flatbuffer_type_converters.cpp | 13 +++++----- .../details/flatbuffer_type_converters_UT.cpp | 7 +++--- .../src/configuration/details/lm_flatcfg.fbs | 6 ++--- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index 1678691a9..1aadffcf6 100644 --- a/score/launch_manager/docs/user_guide/configuration.rst +++ b/score/launch_manager/docs/user_guide/configuration.rst @@ -209,7 +209,7 @@ component_properties (object) * ``"Running"``: The process has started and reached its running state. * ``"Terminated"``: The process has started, reached its running state, and then terminated successfully. * **file_state** (object, optional) - * **Description:** Specifies a ready condition based on the existence state of a file at a given path. + * **Description:** Specifies a ready condition based on the existence of a file at a given path. * **Properties:** * **file_path** (string, required) * **Description:** Specifies the absolute path to the file being watched. @@ -217,12 +217,12 @@ component_properties (object) * **Description:** Specifies the required existence state of the file. * **Allowed Values:** * ``"Exists"``: The component is ready when the file at ``file_path`` exists. - * ``"Deleted"``: The component is ready when the file at ``file_path`` is deleted. + * ``"NotExisting"``: The component is ready when the file at ``file_path`` does not exist. * **Default:** ``"Exists"`` - * **polling_interval** (integer, optional) - * **Description:** Specifies the time interval, in milliseconds, at which the **Launch Manager** checks the file existence state. + * **polling_interval** (number, optional) + * **Description:** Specifies the time interval, in seconds (e.g., ``0.3`` for 300 milliseconds), at which the **Launch Manager** checks the file existence state. * **Constraint:** Must be greater than 0. - * **Default:** ``10`` + * **Default:** ``0.01`` .. _lm_conf_deployment_config_object_: diff --git a/score/launch_manager/src/daemon/src/configuration/config.hpp b/score/launch_manager/src/daemon/src/configuration/config.hpp index b4a2c09d0..a100daec4 100644 --- a/score/launch_manager/src/daemon/src/configuration/config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/config.hpp @@ -42,7 +42,7 @@ enum class ProcessState : uint8_t enum class FileExistenceState : uint8_t { Exists = 0, - Deleted, + NotExisting, }; struct ComponentAliveSupervision diff --git a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json index 990a9411d..98b598d8f 100644 --- a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json +++ b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json @@ -92,25 +92,25 @@ }, "file_state": { "type": "object", - "description": "Specifies a ready condition based on the existence state of a file at a given path.", + "description": "Specifies a ready condition based on the existence of a file at a given path.", "properties": { "file_path": { "type": "string", - "pattern": "^/.*", + "pattern": "^/(?:[^/]+(?:/[^/]+)*)$", "description": "Specifies the absolute path to the file being watched." }, "state": { "type": "string", "enum": [ "Exists", - "Deleted" + "NotExisting" ], - "description": "Specifies the required existence state of the file. 'Exists': the file must be present at 'file_path'. 'Deleted': the file must be absent from 'file_path'. Defaults to 'Exists' if not specified." + "description": "Specifies the required existence of the file. 'Exists': the file must be present at 'file_path'. 'NotExisting': the file must be absent from 'file_path'. Defaults to 'Exists' if not specified." }, "polling_interval": { - "type": "integer", + "type": "number", "exclusiveMinimum": 0, - "description": "Specifies the time interval, in milliseconds, at which the Launch Manager checks the file existence state." + "description": "Specifies the time interval, in seconds (e.g., '0.3' for 300 milliseconds), at which the Launch Manager checks the file existence. Defaults to 10 milliseconds." } }, "required": [ @@ -119,7 +119,18 @@ "additionalProperties": false } }, - "required": [], + "oneOf": [ + { + "required": [ + "process_state" + ] + }, + { + "required": [ + "file_state" + ] + } + ], "additionalProperties": false } }, diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 20e0bdfdd..05f9cdaa7 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -112,8 +112,8 @@ FileExistenceState convertFileExistenceState(fb::FileExistenceState fb_state) { switch (fb_state) { - case fb::FileExistenceState::Deleted: - return FileExistenceState::Deleted; + case fb::FileExistenceState::NotExisting: + return FileExistenceState::NotExisting; case fb::FileExistenceState::Exists: return FileExistenceState::Exists; } @@ -322,10 +322,11 @@ std::optional convertFileState(const fb::FileState* fb_fs) SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( fb_fs->file_path(), "FileState::file_path must never be nullptr as it is required in the schema"); - return FileState{ - fb_fs->file_path()->str(), - convertFileExistenceState(fb_fs->state()), - std::chrono::milliseconds{fb_fs->polling_interval()}}; + const auto polling_interval_seconds = fb_fs->polling_interval(); + const auto polling_interval_ms = + std::chrono::duration_cast(std::chrono::duration(polling_interval_seconds)); + + return FileState{fb_fs->file_path()->str(), convertFileExistenceState(fb_fs->state()), polling_interval_ms}; } std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc) diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index b50914bf9..ac43e84d9 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -636,7 +636,8 @@ TEST_F(ConverterTest, ConvertFileExistenceStateMapsBothValues) { RecordProperty("Description", "convertFileExistenceState maps both enum values correctly."); EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Exists), Eq(FileExistenceState::Exists)); - EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Deleted), Eq(FileExistenceState::Deleted)); + EXPECT_THAT( + details::convertFileExistenceState(fb::FileExistenceState::NotExisting), Eq(FileExistenceState::NotExisting)); } TEST_F(ConverterTest, ConvertFileStateNullReturnsNullopt) @@ -650,14 +651,14 @@ TEST_F(ConverterTest, ConvertFileStateValid) { RecordProperty("Description", "convertFileState maps file_path and an explicit state correctly."); ::flatbuffers::FlatBufferBuilder fbb; - auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Deleted); + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::NotExisting); fbb.Finish(fs); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); auto result = details::convertFileState(ptr); ASSERT_THAT(result.has_value(), IsTrue()); EXPECT_THAT(result->file_path, Eq("/tmp/ready")); - EXPECT_THAT(result->state, Eq(FileExistenceState::Deleted)); + EXPECT_THAT(result->state, Eq(FileExistenceState::NotExisting)); } TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) diff --git a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs index 76cdf9229..c5c636638 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs @@ -29,7 +29,7 @@ enum ProcessState : byte { // Specifies the required existence state of a watched file. enum FileExistenceState : byte { Exists = 0, - Deleted = 1 + NotExisting = 1 } // Scheduling policy for a component's initial thread. @@ -65,8 +65,8 @@ table FileState { file_path:string (required); // required // Existence state of the file. Defaults to Exists if not specified. state:FileExistenceState = Exists; // optional, defaults to Exists - // Time in ms to wait between each poll if the file is present. - polling_interval: uint32 = 10; //optional, defaults to 10ms + // Time in seconds to wait between each poll if the file is present. + polling_interval: double = 0.01; //optional, defaults to 0.01s (10ms) } // Defines the conditions that determine when the component enters the ready state.