diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index b6499c423..8bdb0821b 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 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_: diff --git a/score/launch_manager/src/daemon/src/configuration/config.hpp b/score/launch_manager/src/daemon/src/configuration/config.hpp index 49810d483..5a2be9d20 100644 --- a/score/launch_manager/src/daemon/src/configuration/config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/config.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include namespace score::mw::launch_manager::configuration @@ -37,6 +38,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 +59,15 @@ struct ApplicationProfile std::optional alive_supervision; }; -struct ReadyCondition +struct FileState { - ProcessState process_state{ProcessState::Running}; + std::string file_path; + FileExistenceState state{FileExistenceState::Exists}; }; + +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..a1c3f2ff0 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,29 @@ "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" + ], + "pattern": "^/.*", + "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": [], @@ -488,4 +511,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 4b2ecd3c3..33461f1dc 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp @@ -19,23 +19,28 @@ #include #include #include +#include #include #include -namespace score::mw::launch_manager::configuration { +namespace score::mw::launch_manager::configuration +{ -namespace { +namespace +{ constexpr const char* kAliveInterfaceEnvName = "LCM_ALIVE_INTERFACE_PATH"; constexpr uint32_t kDefaultProcessExecutionError = 1U; -uint64_t defaultProcessorAffinityMask() { +uint64_t defaultProcessorAffinityMask() +{ return (1ULL << score::lcm::internal::osal::getNumCores()) - 1ULL; } } // namespace -score::lcm::internal::osal::CommsType ConfigurationAdapter::mapApplicationType( - ApplicationType app_type) const { - switch (app_type) { +score::lcm::internal::osal::CommsType ConfigurationAdapter::mapApplicationType(ApplicationType app_type) const +{ + switch (app_type) + { case ApplicationType::Reporting: case ApplicationType::ReportingAndSupervised: return score::lcm::internal::osal::CommsType::kReporting; @@ -47,20 +52,25 @@ score::lcm::internal::osal::CommsType ConfigurationAdapter::mapApplicationType( } } -bool ConfigurationAdapter::initialize(const Config& config) { +bool ConfigurationAdapter::initialize(const Config& config) +{ return buildFromConfig(config); } -void ConfigurationAdapter::deinitialize() { - for (auto& process_group : process_groups_) { - for (auto& process : process_group.processes_) { - for (size_t i = 0U; - i < score::lcm::internal::kArgvArraySize && process.startup_config_.argv_[i] != nullptr; - ++i) { +void ConfigurationAdapter::deinitialize() +{ + for (auto& process_group : process_groups_) + { + for (auto& process : process_group.processes_) + { + for (size_t i = 0U; i < score::lcm::internal::kArgvArraySize && process.startup_config_.argv_[i] != nullptr; + ++i) + { free(const_cast(process.startup_config_.argv_[i])); process.startup_config_.argv_[i] = nullptr; } - for (size_t i = 0U; process.startup_config_.envp_[i] != nullptr; ++i) { + for (size_t i = 0U; process.startup_config_.envp_[i] != nullptr; ++i) + { free(process.startup_config_.envp_[i]); process.startup_config_.envp_[i] = nullptr; } @@ -72,7 +82,8 @@ void ConfigurationAdapter::deinitialize() { void ConfigurationAdapter::fillStartupConfigFromDeployment( const ComponentConfig& comp, - score::lcm::internal::osal::OsalConfig& startup) const { + score::lcm::internal::osal::OsalConfig& startup) const +{ const auto& deploy = comp.deployment_config; const auto& props = comp.component_properties; @@ -91,7 +102,8 @@ void ConfigurationAdapter::fillStartupConfigFromDeployment( startup.resource_limits_.cpu_ = 0U; startup.resource_limits_.data_ = 0U; startup.resource_limits_.as_ = 0U; - if (deploy.sandbox.max_memory_usage.has_value()) { + if (deploy.sandbox.max_memory_usage.has_value()) + { startup.resource_limits_.as_ = *deploy.sandbox.max_memory_usage; } @@ -100,33 +112,38 @@ void ConfigurationAdapter::fillStartupConfigFromDeployment( void ConfigurationAdapter::fillStartupArguments( const ComponentProperties& props, - score::lcm::internal::osal::OsalConfig& startup) const { + score::lcm::internal::osal::OsalConfig& startup) const +{ // strdup() returns nullptr on OOM. On this embedded target, OOM during daemon // startup is unrecoverable — the OS will terminate the process. size_t arg_index = 0U; startup.argv_[arg_index++] = strdup(startup.executable_path_.c_str()); - assert(props.process_arguments.size() <= score::lcm::internal::kMaxArg - && "Too many process arguments for argv array"); - size_t max_args = std::min(props.process_arguments.size(), - static_cast(score::lcm::internal::kMaxArg)); - for (size_t i = 0U; i < max_args; ++i) { + assert( + props.process_arguments.size() <= score::lcm::internal::kMaxArg && "Too many process arguments for argv array"); + size_t max_args = std::min(props.process_arguments.size(), static_cast(score::lcm::internal::kMaxArg)); + for (size_t i = 0U; i < max_args; ++i) + { startup.argv_[arg_index++] = strdup(props.process_arguments[i].c_str()); } } size_t ConfigurationAdapter::fillStartupEnvironment( const DeploymentConfig& deploy, - score::lcm::internal::osal::OsalConfig& startup) const { + score::lcm::internal::osal::OsalConfig& startup) const +{ size_t env_index = 0U; - assert(deploy.environmental_variables.size() + 1U <= score::lcm::internal::kMaxEnv - && "Too many environmental variables for envp array"); - size_t max_env = std::min(deploy.environmental_variables.size(), - static_cast(score::lcm::internal::kMaxEnv)); + assert( + deploy.environmental_variables.size() + 1U <= score::lcm::internal::kMaxEnv && + "Too many environmental variables for envp array"); + size_t max_env = + std::min(deploy.environmental_variables.size(), static_cast(score::lcm::internal::kMaxEnv)); size_t env_count = 0; - for (const auto& ev : deploy.environmental_variables) { - if (env_count >= max_env) { + for (const auto& ev : deploy.environmental_variables) + { + if (env_count >= max_env) + { break; } @@ -141,11 +158,13 @@ size_t ConfigurationAdapter::fillStartupEnvironment( void ConfigurationAdapter::appendAliveInterfaceEnvironment( const ComponentConfig& comp, size_t& env_index, - score::lcm::internal::osal::OsalConfig& startup) const { + score::lcm::internal::osal::OsalConfig& startup) const +{ bool is_supervised = comp.component_properties.application_profile.application_type == ApplicationType::ReportingAndSupervised || comp.component_properties.application_profile.application_type == ApplicationType::StateManager; - if (!is_supervised || env_index >= static_cast(score::lcm::internal::kMaxEnv)) { + if (!is_supervised || env_index >= static_cast(score::lcm::internal::kMaxEnv)) + { return; } @@ -154,7 +173,8 @@ void ConfigurationAdapter::appendAliveInterfaceEnvironment( startup.envp_[env_index++] = strdup(iface_path.c_str()); } -PgManagerConfig ConfigurationAdapter::buildPgManagerConfig(const ComponentConfig& comp) const { +PgManagerConfig ConfigurationAdapter::buildPgManagerConfig(const ComponentConfig& comp) const +{ PgManagerConfig pgm{}; const auto& deploy = comp.deployment_config; const auto& props = comp.component_properties; @@ -164,29 +184,51 @@ PgManagerConfig ConfigurationAdapter::buildPgManagerConfig(const ComponentConfig pgm.termination_timeout_ms_ = std::chrono::milliseconds(deploy.shutdown_timeout_ms); pgm.execution_error_code_ = kDefaultProcessExecutionError; pgm.number_of_restart_attempts = 0U; - if (deploy.ready_recovery_action.has_value()) { + if (deploy.ready_recovery_action.has_value()) + { pgm.number_of_restart_attempts = deploy.ready_recovery_action->number_of_attempts; } return pgm; } -DependencyList ConfigurationAdapter::buildDependencyList(const ComponentProperties& props) const { +DependencyList ConfigurationAdapter::buildDependencyList(const ComponentProperties& props) const +{ DependencyList dependencies; dependencies.reserve(props.depends_on.size()); - for (const auto& dep_name : props.depends_on) { + for (const auto& dep_name : props.depends_on) + { + auto dep_it = component_by_name_.find(dep_name); + if (dep_it == component_by_name_.end()) + { + // Couldn't find component, continue + continue; + } + + 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()) { - 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; - } + if (dep_props.ready_condition.has_value()) + { + 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_ASSERT_MESSAGE(false, "FileState is not yet supported"); + return; + } + }, + dep_props.ready_condition.value()); } dep.target_process_id_ = IdentifierHash{dep_name}; @@ -196,9 +238,8 @@ DependencyList ConfigurationAdapter::buildDependencyList(const ComponentProperti return dependencies; } -OsProcess ConfigurationAdapter::buildOsProcess( - const ComponentConfig& comp, - uint32_t process_index) const { +OsProcess ConfigurationAdapter::buildOsProcess(const ComponentConfig& comp, uint32_t process_index) const +{ OsProcess os_process{}; os_process.process_id_ = IdentifierHash{comp.name}; os_process.process_number_ = process_index; @@ -221,31 +262,39 @@ void ConfigurationAdapter::resolveDependsOnEntry( const std::string& dep_name, const DependsOnMap& depends_on_by_name, std::vector& indexes, - std::set& visited) const { - if (!visited.insert(dep_name).second) { + std::set& visited) const +{ + if (!visited.insert(dep_name).second) + { return; } bool found = false; auto comp_it = component_to_process_index_.find(dep_name); - if (comp_it != component_to_process_index_.end()) { + if (comp_it != component_to_process_index_.end()) + { found = true; - if (std::find(indexes.begin(), indexes.end(), comp_it->second) == indexes.end()) { + if (std::find(indexes.begin(), indexes.end(), comp_it->second) == indexes.end()) + { indexes.push_back(comp_it->second); } auto comp_cfg = component_by_name_.find(dep_name); - if (comp_cfg != component_by_name_.end()) { - for (const auto& sub_dep : comp_cfg->second->component_properties.depends_on) { + if (comp_cfg != component_by_name_.end()) + { + for (const auto& sub_dep : comp_cfg->second->component_properties.depends_on) + { resolveDependsOnEntry(sub_dep, depends_on_by_name, indexes, visited); } } } auto dep_it = depends_on_by_name.find(dep_name); - if (dep_it != depends_on_by_name.end()) { + if (dep_it != depends_on_by_name.end()) + { found = true; - for (const auto& sub_dep : *dep_it->second) { + for (const auto& sub_dep : *dep_it->second) + { resolveDependsOnEntry(sub_dep, depends_on_by_name, indexes, visited); } } @@ -256,33 +305,37 @@ void ConfigurationAdapter::resolveDependsOnEntry( ProcessGroupState ConfigurationAdapter::buildProcessGroupState( const std::string& state_name, const std::vector& depends_on, - const DependsOnMap& depends_on_by_name) const { + const DependsOnMap& depends_on_by_name) const +{ ProcessGroupState state; state.name_ = IdentifierHash{state_name}; std::set visited; - for (const auto& dep_name : depends_on) { + for (const auto& dep_name : depends_on) + { resolveDependsOnEntry(dep_name, depends_on_by_name, state.process_indexes_, visited); } return state; } -std::vector ConfigurationAdapter::buildProcessGroupStates( - const Config& config) const { +std::vector ConfigurationAdapter::buildProcessGroupStates(const Config& config) const +{ std::vector states; states.push_back(ProcessGroupState{IdentifierHash{"MainPG/Off"}, {}}); const auto& run_targets = config.runTargets(); DependsOnMap depends_on_by_name; - for (const auto& rt : run_targets) { + for (const auto& rt : run_targets) + { depends_on_by_name[rt.name] = &rt.depends_on; } const auto& fallback_cfg = config.fallbackRunTarget(); depends_on_by_name["fallback_run_target"] = &fallback_cfg.depends_on; - for (const auto& rt : run_targets) { + for (const auto& rt : run_targets) + { states.push_back(buildProcessGroupState("MainPG/" + rt.name, rt.depends_on, depends_on_by_name)); } @@ -290,11 +343,16 @@ std::vector ConfigurationAdapter::buildProcessGroupStates( return states; } -void ConfigurationAdapter::resolveDependencyIndexes(std::vector& processes) { - for (auto& proc : processes) { - for (auto& dep : proc.dependencies_) { - for (uint32_t idx = 0; idx < processes.size(); ++idx) { - if (processes[idx].process_id_ == dep.target_process_id_) { +void ConfigurationAdapter::resolveDependencyIndexes(std::vector& processes) +{ + for (auto& proc : processes) + { + for (auto& dep : proc.dependencies_) + { + for (uint32_t idx = 0; idx < processes.size(); ++idx) + { + if (processes[idx].process_id_ == dep.target_process_id_) + { dep.os_process_index_ = idx; break; } @@ -303,7 +361,8 @@ void ConfigurationAdapter::resolveDependencyIndexes(std::vector& proc } } -bool ConfigurationAdapter::buildFromConfig(const Config& config) { +bool ConfigurationAdapter::buildFromConfig(const Config& config) +{ const std::string initial_run_target_name = std::string(config.initialRunTarget()); ProcessGroup pg; @@ -316,12 +375,14 @@ bool ConfigurationAdapter::buildFromConfig(const Config& config) { component_by_name_.clear(); component_to_process_index_.clear(); - for (const auto& comp : config.components()) { + for (const auto& comp : config.components()) + { component_by_name_[comp.name] = ∁ } uint32_t process_index = 0; - for (const auto& comp : config.components()) { + for (const auto& comp : config.components()) + { component_to_process_index_[comp.name] = process_index; pg.processes_.push_back(buildOsProcess(comp, process_index)); ++process_index; @@ -334,58 +395,67 @@ bool ConfigurationAdapter::buildFromConfig(const Config& config) { process_group_names_.push_back(pg_name); main_pg_startup_state_ = score::lcm::internal::ProcessGroupStateID{ - pg_name, - IdentifierHash{std::string("MainPG/") + initial_run_target_name} - }; + pg_name, IdentifierHash{std::string("MainPG/") + initial_run_target_name}}; - LM_LOG_DEBUG() << "ConfigurationAdapter: Built configuration with " - << process_groups_[0].processes_.size() << " processes and " - << process_groups_[0].states_.size() << " states"; + LM_LOG_DEBUG() << "ConfigurationAdapter: Built configuration with " << process_groups_[0].processes_.size() + << " processes and " << process_groups_[0].states_.size() << " states"; return true; } -std::optional*> ConfigurationAdapter::getListOfProcessGroups() const { - if (!process_group_names_.empty()) { +std::optional*> ConfigurationAdapter::getListOfProcessGroups() const +{ + if (!process_group_names_.empty()) + { return &process_group_names_; } return std::nullopt; } -std::optional ConfigurationAdapter::getSoftwareCluster(const IdentifierHash& process_group_id) const { +std::optional ConfigurationAdapter::getSoftwareCluster(const IdentifierHash& process_group_id) const +{ auto pg = getProcessGroupByID(process_group_id); - if (pg) { + if (pg) + { return pg->sw_cluster_; } return std::nullopt; } -std::optional ConfigurationAdapter::getNumberOfOsProcesses(const IdentifierHash& pg_name) const { +std::optional ConfigurationAdapter::getNumberOfOsProcesses(const IdentifierHash& pg_name) const +{ auto pg = getProcessGroupByID(pg_name); - if (pg) { + if (pg) + { return static_cast(pg->processes_.size()); } return std::nullopt; } -IdentifierHash ConfigurationAdapter::getNameOfOffState(const IdentifierHash& pg_name) const { +IdentifierHash ConfigurationAdapter::getNameOfOffState(const IdentifierHash& pg_name) const +{ auto pg = getProcessGroupByID(pg_name); - if (pg) { + if (pg) + { return pg->off_state_; } return IdentifierHash{"Off"}; } -IdentifierHash ConfigurationAdapter::getNameOfRecoveryState(const IdentifierHash& pg_name) const { +IdentifierHash ConfigurationAdapter::getNameOfRecoveryState(const IdentifierHash& pg_name) const +{ auto pg = getProcessGroupByID(pg_name); - if (pg) { + if (pg) + { return pg->recovery_state_; } return IdentifierHash{"Recovery"}; } -std::optional ConfigurationAdapter::getMainPGStartupState() const { - if (!process_groups_.empty()) { +std::optional ConfigurationAdapter::getMainPGStartupState() const +{ + if (!process_groups_.empty()) + { return &main_pg_startup_state_; } @@ -394,50 +464,64 @@ std::optional ConfigurationAda } std::optional*> ConfigurationAdapter::getProcessIndexesList( - const score::lcm::internal::ProcessGroupStateID& pg_state_id) const { + const score::lcm::internal::ProcessGroupStateID& pg_state_id) const +{ auto state = getProcessGroupStateByID(pg_state_id); - if (state) { + if (state) + { return &state->process_indexes_; } - LM_LOG_DEBUG() << "Process group state '" << pg_state_id.pg_state_name_ - << "' not found in group '" << pg_state_id.pg_name_ << "'."; + LM_LOG_DEBUG() << "Process group state '" << pg_state_id.pg_state_name_ << "' not found in group '" + << pg_state_id.pg_name_ << "'."; return std::nullopt; } std::optional ConfigurationAdapter::getOsProcessConfiguration( - const IdentifierHash& pg_name, const uint32_t index) const { - if (auto pg = getProcessGroupByNameAndIndex(pg_name, index)) { + const IdentifierHash& pg_name, + const uint32_t index) const +{ + if (auto pg = getProcessGroupByNameAndIndex(pg_name, index)) + { return &(*pg)->processes_[index]; } - LM_LOG_DEBUG() << "Unable to retrieve process configuration for process group" << pg_name - << "with index" << index; + LM_LOG_DEBUG() << "Unable to retrieve process configuration for process group" << pg_name << "with index" << index; return std::nullopt; } std::optional ConfigurationAdapter::getOsProcessDependencies( - const IdentifierHash& pg_name, const uint32_t index) const { - if (auto pg = getProcessGroupByNameAndIndex(pg_name, index)) { + const IdentifierHash& pg_name, + const uint32_t index) const +{ + if (auto pg = getProcessGroupByNameAndIndex(pg_name, index)) + { return &(*pg)->processes_[index].dependencies_; } - LM_LOG_DEBUG() << "Unable to retrieve process dependencies for process group" << pg_name - << "with index" << index; + LM_LOG_DEBUG() << "Unable to retrieve process dependencies for process group" << pg_name << "with index" << index; return std::nullopt; } -ProcessGroup* ConfigurationAdapter::getProcessGroupByID(const IdentifierHash& pg_name) const { - for (const auto& pg : process_groups_) { - if (pg.name_ == pg_name) { +ProcessGroup* ConfigurationAdapter::getProcessGroupByID(const IdentifierHash& pg_name) const +{ + for (const auto& pg : process_groups_) + { + if (pg.name_ == pg_name) + { return const_cast(&pg); } } return nullptr; } -ProcessGroupState* ConfigurationAdapter::getProcessGroupStateByID(const score::lcm::internal::ProcessGroupStateID& pg_id) const { +ProcessGroupState* ConfigurationAdapter::getProcessGroupStateByID( + const score::lcm::internal::ProcessGroupStateID& pg_id) const +{ ProcessGroup* pg = getProcessGroupByID(pg_id.pg_name_); - if (pg) { - for (auto& state : pg->states_) { - if (state.name_ == pg_id.pg_state_name_) { + if (pg) + { + for (auto& state : pg->states_) + { + if (state.name_ == pg_id.pg_state_name_) + { return &state; } } @@ -446,14 +530,20 @@ ProcessGroupState* ConfigurationAdapter::getProcessGroupStateByID(const score::l } std::optional ConfigurationAdapter::getProcessGroupByNameAndIndex( - const IdentifierHash& pg_name, const uint32_t index) const { + const IdentifierHash& pg_name, + const uint32_t index) const +{ auto pg = getProcessGroupByID(pg_name); - if (pg) { - if (index < pg->processes_.size()) { + if (pg) + { + if (index < pg->processes_.size()) + { return pg; } LM_LOG_DEBUG() << "Process index" << index << "is out of bounds in process group" << pg_name; - } else { + } + else + { LM_LOG_DEBUG() << "Process group not found:" << pg_name; } return std::nullopt; 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 1fdcc563a..bf819fb15 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp @@ -100,17 +100,25 @@ class ConfigurationAdapter final { 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; 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..326adfbdb 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,19 +313,53 @@ 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::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 ReadyCondition{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 ReadyCondition{*file_state}; } - return result; } score::cpp::expected convertComponentProperties( @@ -338,12 +385,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 f1a478586..48917f081 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); @@ -107,7 +111,7 @@ convertComponentAliveSupervision(const fb::ComponentAliveSupervision* fb_cas); [[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 2c62fef23..187da401a 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 @@ -561,7 +561,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; @@ -571,12 +578,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, ConvertReadyConditionWithFileState) +{ + 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, ConvertReadyConditionMissingProcessStateReturnsError) +TEST_F(ConverterTest, ConvertReadyConditionWithNeitherStateReturnsNullopt) { - RecordProperty("Description", "Missing process_state returns InvalidFormat."); + RecordProperty("Description", "convertReadyCondition with neither process_state nor file_state returns nullopt."); ::flatbuffers::FlatBufferBuilder fbb; auto rc = fb::CreateReadyCondition(fbb); fbb.Finish(rc); @@ -584,7 +619,48 @@ 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, 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/new_lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/new_lm_flatcfg.fbs index 98e716324..0fc4c4032 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,21 @@ 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. +// 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.