Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions score/launch_manager/docs/user_guide/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
MaciejKaszynski marked this conversation as resolved.
* **Description:** Specifies a ready condition based on the existence state of a file at a given path.
* **Properties:**
Comment thread
MaciejKaszynski marked this conversation as resolved.
* **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.

@NicolasFussberger NicolasFussberger Aug 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any files which cannot be watched via inotify and thus we need to poll for their existence with some to-be-configured interval?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed only containers would be problematic here, and I think for now since we do not support containers anyways I don't think we should support this yet. I think in the future we could have an optional polling_rate var that when set would switch to using polling but don't think it makes sense to pay so much of a slow down to support a very specific scenario.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, support for polling only needs to be added if/when we support containers.

* **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_:

Expand Down
16 changes: 14 additions & 2 deletions score/launch_manager/src/daemon/src/configuration/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
#define CONFIG_HPP

#include <sys/types.h>
#include <chrono>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>

namespace score::mw::launch_manager::configuration
Expand All @@ -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{};
Expand All @@ -52,11 +60,15 @@ struct ApplicationProfile
std::optional<ComponentAliveSupervision> 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<ProcessState, FileState>;

struct ComponentProperties
{
std::string binary_name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
MaciejKaszynski marked this conversation as resolved.
"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": [],
Expand Down Expand Up @@ -488,4 +516,4 @@
"initial_run_target"
],
"additionalProperties": false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <algorithm>
#include <cassert>
#include <cstring>
#include <iostream>
#include <map>
#include <set>

Expand Down Expand Up @@ -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<decltype(arg)>;

if constexpr (std::is_same_v<argT, score::mw::launch_manager::configuration::ProcessState>)
{
dep.process_state_ = arg == ProcessState::Running ? score::lcm::ProcessState::kRunning
: score::lcm::ProcessState::kTerminated;
return;
}
else if constexpr (std::is_same_v<argT, score::mw::launch_manager::configuration::FileState>)
{
SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE("FileState is not yet supported");
return;
}
},
dep_props.ready_condition.value());
}

dep.target_process_id_ = IdentifierHash{dep_name};
Expand Down Expand Up @@ -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);
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProcessGroupState> buildProcessGroupStates(const Config& config) const;
ProcessGroupState buildProcessGroupState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComponentConfig> 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<RunTargetConfig> 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<ComponentConfig> 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<RunTargetConfig> 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
Original file line number Diff line number Diff line change
Expand Up @@ -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"};

Expand Down Expand Up @@ -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>(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"));
Expand Down
Loading
Loading