diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index a51caa11d..56ec11eca 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** (number, optional) + * **Description:** Specifies the time interval, in seconds (e.g., ``0.5`` for 500 milliseconds), at which the **Launch Manager** checks the file existence state. + * **Constraint:** Must be greater than 0. + * **Default:** ``10ms`` .. _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 e7e6d7c71..2f98a1228 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::launch_manager::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..7f6d9113a 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": "number", + "exclusiveMinimum": 0, + "description": "Specifies the time interval, in seconds (e.g., '0.5' for 500 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 4e1605f0d..6e68e6af0 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 @@ -199,19 +200,33 @@ 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::lcm::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::lcm::ProcessState::kRunning - : score::lcm::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::lcm::ProcessState::kRunning + : score::lcm::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}; @@ -252,11 +267,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); @@ -275,14 +288,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 146b0ee50..d6f5ef24e 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp @@ -109,17 +109,24 @@ class ConfigurationAdapter final bool buildFromConfig(const Config& config); OsProcess buildOsProcess(const ComponentConfig& comp, uint32_t process_index) const; + void fillStartupConfigFromDeployment(const ComponentConfig& comp, score::lcm::internal::osal::OsalConfig& startup) const; + void fillStartupArguments(const ComponentProperties& props, score::lcm::internal::osal::OsalConfig& startup) const; + size_t fillStartupEnvironment(const DeploymentConfig& deploy, score::lcm::internal::osal::OsalConfig& startup) const; + void appendAliveInterfaceEnvironment( const ComponentConfig& comp, size_t& env_index, score::lcm::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 e2bab7bbb..fa5fd8657 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 @@ -569,5 +569,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::launch_manager::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 64bce2d06..da7111814 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 @@ -35,6 +35,7 @@ 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,7 +259,7 @@ 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(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")); 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 7e2a5eb15..cae68f770 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 3ad609d7f..b44d29af4 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,7 +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( +[[nodiscard]] std::optional convertReadyCondition( const fb::ReadyCondition* fb_rc); /// @brief Converts a FlatBuffer ComponentProperties to the config equivalent. [[nodiscard]] score::cpp::expected convertComponentProperties( 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 a2c215ea6..0a50bf97a 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 98e716324..bcfa74d89 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.