From 79f55a845c7b47ca992442f09c98b0e684831fe1 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 | 11 +++ .../src/daemon/src/configuration/config.hpp | 15 +++- .../config_schema/launch_manager.schema.json | 22 ++++++ .../configuration_adapter_UT.cpp | 10 +-- .../details/flatbuffer_config_loader_UT.cpp | 39 ++++++++++ .../details/flatbuffer_type_converters.cpp | 34 ++++++++- .../details/flatbuffer_type_converters.hpp | 4 ++ .../details/flatbuffer_type_converters_UT.cpp | 71 +++++++++++++++++++ .../configuration/details/new_lm_flatcfg.fbs | 15 ++++ 9 files changed, 213 insertions(+), 8 deletions(-) diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index b6499c423..265dac2bc 100644 --- a/score/launch_manager/docs/user_guide/configuration.rst +++ b/score/launch_manager/docs/user_guide/configuration.rst @@ -208,6 +208,17 @@ 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 ready condition is ready when the file at ``file_path`` exists. + * ``"Deleted"``: The ready condition is ready when teh file at ``file_path`` is deleted. + * **Default:** ``"Exists"`` .. _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 49810d483..7935769fe 100644 --- a/score/launch_manager/src/daemon/src/configuration/config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/config.hpp @@ -37,6 +37,12 @@ enum class ProcessState : uint8_t Terminated = 1 }; +enum class FileExistenceState : uint8_t +{ + Exists = 0, + Deleted, +}; + struct ComponentAliveSupervision { uint32_t reporting_cycle_ms{}; @@ -52,9 +58,16 @@ struct ApplicationProfile std::optional alive_supervision; }; +struct FileState +{ + std::string file_path; + FileExistenceState state{FileExistenceState::Exists}; +}; + struct ReadyCondition { - ProcessState process_state{ProcessState::Running}; + std::optional process_state{ProcessState::Running}; + std::optional file_state; }; struct ComponentProperties 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..1072d5b1d 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,28 @@ "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", + "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." + } + }, + "required": [ + "file_path" + ], + "additionalProperties": false } }, "required": [], 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 e18d2dd0e..56a097317 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 @@ -37,7 +37,7 @@ Config makeMinimalConfig() comp_a.component_properties.application_profile.application_type = ApplicationType::ReportingAndSupervised; comp_a.component_properties.application_profile.is_self_terminating = false; comp_a.component_properties.application_profile.alive_supervision = ComponentAliveSupervision{500, 2, 1, 3}; - comp_a.component_properties.ready_condition = ReadyCondition{ProcessState::Running}; + comp_a.component_properties.ready_condition = ReadyCondition{ProcessState::Running, std::nullopt}; comp_a.deployment_config.ready_timeout_ms = 500; comp_a.deployment_config.shutdown_timeout_ms = 500; comp_a.deployment_config.bin_dir = "/opt/apps"; @@ -54,7 +54,7 @@ Config makeMinimalConfig() comp_b.component_properties.application_profile.application_type = ApplicationType::Native; comp_b.component_properties.application_profile.is_self_terminating = true; comp_b.component_properties.depends_on = {"comp_a"}; - comp_b.component_properties.ready_condition = ReadyCondition{ProcessState::Running}; + comp_b.component_properties.ready_condition = ReadyCondition{ProcessState::Running, std::nullopt}; comp_b.deployment_config.ready_timeout_ms = 1000; comp_b.deployment_config.shutdown_timeout_ms = 1000; comp_b.deployment_config.bin_dir = "/opt/apps"; @@ -347,7 +347,7 @@ TEST(ConfigurationAdapterReadyConditionTest, DependencyUsesTargetComponentReadyC comp_a.name = "comp_a"; comp_a.component_properties.application_profile.application_type = ApplicationType::Native; comp_a.component_properties.application_profile.is_self_terminating = true; - comp_a.component_properties.ready_condition = ReadyCondition{ProcessState::Terminated}; + comp_a.component_properties.ready_condition = ReadyCondition{ProcessState::Terminated, std::nullopt}; comp_a.deployment_config.bin_dir = "/opt"; comp_a.component_properties.binary_name = "comp_a"; comp_a.deployment_config.working_dir = "/tmp"; @@ -360,7 +360,7 @@ TEST(ConfigurationAdapterReadyConditionTest, DependencyUsesTargetComponentReadyC 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.ready_condition = ReadyCondition{ProcessState::Running, std::nullopt}; comp_b.component_properties.depends_on = {"comp_a"}; comp_b.deployment_config.bin_dir = "/opt"; comp_b.component_properties.binary_name = "comp_b"; @@ -434,7 +434,7 @@ TEST(ConfigurationAdapterReadyConditionTest, DependencyDefaultsToRunningWhenTarg 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::Terminated}; + comp_b.component_properties.ready_condition = ReadyCondition{ProcessState::Terminated, std::nullopt}; comp_b.component_properties.depends_on = {"comp_a"}; comp_b.deployment_config.bin_dir = "/opt"; comp_b.component_properties.binary_name = "comp_b"; 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 f5877b710..5fa4c7719 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 @@ -246,6 +246,45 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponent) 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, fb::ProcessState::Running, 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->process_state, Eq(ProcessState::Running)); + ASSERT_THAT(comp.component_properties.ready_condition->file_state.has_value(), IsTrue()); + EXPECT_THAT(comp.component_properties.ready_condition->file_state->file_path, Eq("/tmp/ready")); + EXPECT_THAT(comp.component_properties.ready_condition->file_state->state, Eq(FileExistenceState::Exists)); +} + 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 aa4ed0eec..84e8794e3 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 @@ -34,8 +34,9 @@ namespace { template -score::cpp::expected requireScalarValue(const ::flatbuffers::Optional& field, - const std::string_view field_name) +score::cpp::expected requireScalarValue( + const ::flatbuffers::Optional& field, + const std::string_view field_name) { if (!field.has_value()) { @@ -107,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: + default: + return FileExistenceState::Exists; + } +} + score::cpp::expected convertSchedulingPolicy(fb::SchedulingPolicy policy) { switch (policy) @@ -300,6 +313,17 @@ score::cpp::expected convertApplicatio return result; } +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())}; +} + score::cpp::expected convertReadyCondition(const fb::ReadyCondition* fb_rc) { ReadyCondition result{}; @@ -311,7 +335,13 @@ score::cpp::expected convertReadyCondition return score::cpp::make_unexpected(process_state.error()); } result.process_state = convertProcessState(*process_state); + result.file_state = convertFileState(fb_rc->file_state()); } + + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + !(result.process_state == std::nullopt && result.file_state == std::nullopt), + "At least one ready condition is required, exiting as configuration is invalid"); + 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 f1a478586..d83f19402 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); 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 2c62fef23..74054fb20 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 @@ -587,6 +587,77 @@ TEST_F(ConverterTest, ConvertReadyConditionMissingProcessStateReturnsError) EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } +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, ConvertReadyConditionWithFileState) +{ + RecordProperty("Description", "convertReadyCondition maps a present file_state alongside process_state."); + ::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(), IsTrue()); + EXPECT_THAT(result->process_state, Eq(ProcessState::Running)); + ASSERT_THAT(result->file_state.has_value(), IsTrue()); + EXPECT_THAT(result->file_state->file_path, Eq("/tmp/ready")); + EXPECT_THAT(result->file_state->state, Eq(FileExistenceState::Exists)); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithoutFileStateLeavesNullopt) +{ + RecordProperty("Description", "convertReadyCondition leaves file_state as nullopt when absent."); + ::flatbuffers::FlatBufferBuilder fbb; + auto rc = fb::CreateReadyCondition(fbb, fb::ProcessState::Running); + fbb.Finish(rc); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertReadyCondition(ptr); + ASSERT_THAT(result.has_value(), IsTrue()); + EXPECT_THAT(result->file_state.has_value(), IsFalse()); +} + TEST_F(ConverterTest, ConvertSandboxValid) { RecordProperty("Description", "convertSandbox maps all fields including optional ones."); diff --git a/score/launch_manager/src/daemon/src/configuration/details/new_lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/new_lm_flatcfg.fbs index 98e716324..8f0d4f2f6 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/new_lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/details/new_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,18 @@ 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 +} + // Defines the conditions that determine when the component enters the ready state. table ReadyCondition { process_state:ProcessState = null; // required + file_state:FileState; // optional } // Defines essential characteristics of a software component. From 29e09e3af444c4edd57c7c7c98a29221cee84a27 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Mon, 3 Aug 2026 09:38:25 +0100 Subject: [PATCH 2/2] Fixing spelling --- score/launch_manager/docs/user_guide/configuration.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index 265dac2bc..8bdb0821b 100644 --- a/score/launch_manager/docs/user_guide/configuration.rst +++ b/score/launch_manager/docs/user_guide/configuration.rst @@ -216,8 +216,8 @@ component_properties (object) * **state** (string, optional) * **Description:** Specifies the required existence state of the file. * **Allowed Values:** - * ``"Exists"``: The ready condition is ready when the file at ``file_path`` exists. - * ``"Deleted"``: The ready condition is ready when teh file at ``file_path`` is deleted. + * ``"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"`` .. _lm_conf_deployment_config_object_: