diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp index 20564194b..2cb5e2d4f 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp @@ -183,6 +183,8 @@ PgManagerConfig ConfigurationAdapter::buildPgManagerConfig(const ComponentConfig const auto& props = comp.component_properties; pgm.is_self_terminating_ = props.application_profile.is_self_terminating; + pgm.ready_on_termination_ = + props.ready_condition.has_value() && (props.ready_condition->process_state == ProcessState::Terminated); pgm.startup_timeout_ms_ = std::chrono::milliseconds(deploy.ready_timeout_ms); pgm.termination_timeout_ms_ = std::chrono::milliseconds(deploy.shutdown_timeout_ms); pgm.execution_error_code_ = kDefaultProcessExecutionError; diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp index 9cc87683d..ce8cde30a 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp @@ -33,6 +33,7 @@ namespace score::mw::lifecycle::internal::configuration struct PgManagerConfig final { bool is_self_terminating_{}; + bool ready_on_termination_{}; std::chrono::milliseconds startup_timeout_ms_{}; std::chrono::milliseconds termination_timeout_ms_{}; uint32_t number_of_restart_attempts{}; diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp index d07d4daae..097c58c00 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 @@ -223,6 +223,7 @@ TEST_F(ConfigurationAdapterTest, GetOsProcessConfigurationMapsComponentFields) EXPECT_THAT(os_proc->startup_config_.uid_, Eq(1000U)); EXPECT_THAT(os_proc->startup_config_.gid_, Eq(1000U)); EXPECT_THAT(os_proc->pgm_config_.is_self_terminating_, Eq(false)); + EXPECT_THAT(os_proc->pgm_config_.ready_on_termination_, Eq(false)); EXPECT_THAT(os_proc->pgm_config_.startup_timeout_ms_, Eq(std::chrono::milliseconds{500})); EXPECT_THAT(os_proc->pgm_config_.termination_timeout_ms_, Eq(std::chrono::milliseconds{500})); } @@ -493,6 +494,86 @@ TEST(ConfigurationAdapterReadyConditionTest, DependencyDefaultsToRunningWhenTarg adapter.deinitialize(); } +TEST(ConfigurationAdapterReadyConditionTest, ReadyOnTerminationUsesOwnReadyConditionNotDependencies) +{ + RecordProperty( + "Description", + "pgm_config_.ready_on_termination_ is derived from the component's own ready_condition, independently of the " + "ready conditions reached through its dependencies."); + 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 = true; + comp_a.component_properties.ready_condition = ReadyCondition{ProcessState::Terminated}; + 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; + adapter.initialize(config); + + IdentifierHash pg_name{"MainPG"}; + + auto comp_a_result = adapter.getOsProcessConfiguration(pg_name, 0U); + ASSERT_TRUE(comp_a_result.has_value()); + EXPECT_THAT((*comp_a_result)->pgm_config_.ready_on_termination_, Eq(true)) + << "comp_a declares ready_condition Terminated, even though it has no dependencies"; + + auto comp_b_result = adapter.getOsProcessConfiguration(pg_name, 1U); + ASSERT_TRUE(comp_b_result.has_value()); + EXPECT_THAT((*comp_b_result)->pgm_config_.ready_on_termination_, Eq(false)) + << "comp_b declares ready_condition Running, even though it depends on a Terminated component"; + + adapter.deinitialize(); +} + TEST(ConfigurationAdapterFallbackTest, FallbackRunTargetResolvesDependenciesRecursively) { RecordProperty("Description", "Fallback run target resolves transitive component dependencies."); diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/graph.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/graph.cpp index c503c84ef..927b12321 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/graph.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/graph.cpp @@ -89,9 +89,6 @@ void Graph::createProcessInfoNodes(uint32_t num_processes) for (uint32_t process_id = 0U; process_id < num_processes; ++process_id) { LM_LOG_DEBUG() << "Creating process node with id:" << process_id; - auto ready_condition = nodeHasTerminatedDeps(getProcessGroupName(), process_id) - ? ProcessInfoNode::ReadyCondition::kTerminated - : ProcessInfoNode::ReadyCondition::kRunning; const auto* config = configuration_->getOsProcessConfiguration(getProcessGroupName(), process_id).value_or(nullptr); @@ -101,6 +98,10 @@ void Graph::createProcessInfoNodes(uint32_t num_processes) << getProcessGroupName(); } + const auto ready_condition = (config && config->pgm_config_.ready_on_termination_) + ? ProcessInfoNode::ReadyCondition::kTerminated + : ProcessInfoNode::ReadyCondition::kRunning; + const auto index = nodes_.emplace( std::in_place_type, config, @@ -155,18 +156,6 @@ int32_t Graph::getRunTargetIndex(IdentifierHash pg_state) const return -1; } -bool Graph::nodeHasTerminatedDeps(IdentifierHash pg_name, uint32_t node_index) -{ - const DependencyList* dep_list = configuration_->getOsProcessDependencies(pg_name, node_index).value_or(nullptr); - - if (dep_list && dep_list->size() > 0) - { - return (*dep_list)[0].process_state_ == ProcessState::kTerminated; - } - - return false; -} - void Graph::createSuccessorLists(IdentifierHash pg_name) { LM_LOG_DEBUG() << "Creating successor lists for process group" << pg_name; @@ -398,7 +387,7 @@ void Graph::handleComponentEvent(const ComponentEvent& event) using T = std::decay_t; if constexpr (std::is_same_v || std::is_same_v) { - LM_LOG_DEBUG() << "Component " << data.node_index << " finished " + LM_LOG_DEBUG() << "Component" << data.node_index << "finished" << (std::is_same_v ? std::string_view("activation") : std::string_view("deactivation")) << " successfully"; diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/graph.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/graph.hpp index ba383ed08..16b7b3a4d 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/graph.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/graph.hpp @@ -289,9 +289,6 @@ class Graph final void forceKillProcesses(); private: - /// @brief Helper function to identify a node with ready state "Terminated" from the legacy configuration - bool nodeHasTerminatedDeps(IdentifierHash pg_name, uint32_t node_index); - /// @brief Reports that a node has finished executing, enqueuing successors or updating the graph state if a /// transition has finished. void nodeExecuted(uint32_t node, score::cpp::expected_blank error); diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index 806a11d9c..6fd5b83c0 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -44,6 +44,12 @@ ProcessInfoNode::ProcessInfoNode( IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecycle::ProcessState new_state) { + if (new_state == ProcessState::kFailed) + { + // Didn't reach running or startup + return tryReportError(ComponentError::kErrorBeforeReady); + } + ProcessState desired_state{}; switch (ready_condition_) { @@ -54,12 +60,8 @@ IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecy desired_state = ProcessState::kTerminated; break; } - if (new_state == ProcessState::kFailed) - { - // Didn't reach running or startup - return tryReportError(ComponentError::kErrorBeforeReady); - } - if (new_state == desired_state) + // NOTE: Make assumptions over the enumeration values of ProcessState + if (new_state >= desired_state) { return tryReportSuccess(); } @@ -256,7 +258,12 @@ IComponent::RequestResult ProcessInfoNode::startProcess(score::cpp::stop_token s } setState(ProcessState::kRunning); // Can fail if we've terminated already - return tryReportCompletion(ProcessState::kRunning); + + // A self-terminating process may already have exited before startup completed. tryHandleTermination() + // leaves such a node waiting for the startup thread, so report against the state actually reached. + const ProcessState reached_state = + (getState() == ProcessState::kTerminated) ? ProcessState::kTerminated : ProcessState::kRunning; + return tryReportCompletion(reached_state); } void ProcessInfoNode::setupControlClientChannel() diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp index ee9db850c..61f1cdf7c 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp @@ -234,6 +234,31 @@ TEST_F(ProcessInfoNodeStartupTest, SelfTerminating_ExitsBeforeMapInsert_ReturnsS ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kTerminated)); } +TEST_F(ProcessInfoNodeStartupTest, SelfTerminating_TerminatedReadyCondition_ExitsBeforeMapInsert_ReturnsSuccess) +{ + RecordProperty( + "Description", + "A self-terminating process whose ready condition is kTerminated and that exits with status 0 before the map " + "insertion completes reports success from activate() instead of waiting forever."); + + auto node = createProcessInfoNode(osal::CommsType::kNoComms, 0, true, ProcessInfoNode::ReadyCondition::kTerminated); + // Simulate the process exiting before the map insertion happens. + EXPECT_CALL(mock_processIf_, startProcess(_, _, _)) + .WillOnce(DoAll( + InvokeWithoutArgs([node = node.get()] { + node->tryHandleTermination(0); + }), + Return(osal::OsalReturnType::kSuccess))); + EXPECT_CALL(*process_map_, insertIfNotTerminated(_, _)) + .WillOnce(Return(score::mw::lifecycle::internal::SafeProcessMapReturnType::kYield)); + + auto result = node->activate(score::cpp::stop_token{}); + + ASSERT_THAT(result.has_value(), IsTrue()); + ASSERT_THAT(result.value(), Eq(IComponent::RequestState::kSuccess)); + ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kTerminated)); +} + TEST_F(ProcessInfoNodeStartupTest, ActivateAlreadyActiveNode_ReturnsSuccess) { RecordProperty( diff --git a/tests/integration/rt_running_when_process_exits/BUILD b/tests/integration/rt_running_when_process_exits/BUILD new file mode 100644 index 000000000..548caf3c7 --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/BUILD @@ -0,0 +1,49 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +load("//tests/utils/bazel:integration.bzl", "integration_test") + +cc_binary( + name = "filesystem_reader", + srcs = ["filesystem_reader.cpp"], + deps = [ + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "control_client_test_driver", + srcs = ["control_client_test_driver.cpp"], + deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +integration_test( + name = "rt_running_when_process_exits", + timeout = "short", + srcs = ["rt_running_when_process_exits.py"], + binaries = [ + ":control_client_test_driver", + ":filesystem_reader", + ":setup_filesystem.sh", + ":slow_setup.sh", + "//score/launch_manager", + ], + config = ":rt_running_when_process_exits.json", +) diff --git a/tests/integration/rt_running_when_process_exits/control_client_test_driver.cpp b/tests/integration/rt_running_when_process_exits/control_client_test_driver.cpp new file mode 100644 index 000000000..0e888eea6 --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/control_client_test_driver.cpp @@ -0,0 +1,89 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include + +#include +#include + +#include "tests/utils/test_helper/test_helper.hpp" +#include +#include + +namespace +{ +/// @brief Marker file written by slow_setup.sh once it has finished (and is about to exit). +constexpr std::string_view kSlowSetupOutput = "slow_setup_output.txt"; +} // namespace + +// Given a configuration with two run targets, each pulling in a self-terminating component whose +// ready condition is "Terminated" but which differ in whether that component has a dependent: +// +// - run_target_reader: filesystem_reader (ready "Running") depends on setup_filesystem_sh +// (self-terminating, ready "Terminated"). The terminated-ready +// component HAS a dependent. +// - run_target_slow_setup: depends directly on slow_setup_sh (self-terminating, ready +// "Terminated") which has NO dependent component. +// +// In both cases the run target must only report success once the terminated-ready component's +// process has actually exited. Without the fix, graph accounting for such a node happens as soon as +// the process is *started*, so ActivateRunTarget(...).Get() returns while the script is still +// running and its marker file has not been written yet. +TEST(RtRunningWhenProcessExits, ControlClientTestDriver) +{ + score::mw::lifecycle::ControlClient client; + score::cpp::stop_token stop_token; + + ASSERT_TRUE(check_clean({test_end_location})); + // The marker file may be left over from a previous run when executing manually on the host. + // Remove it so that its presence is a reliable signal that slow_setup.sh terminated during + // *this* run. + ASSERT_TRUE(check_clean({kSlowSetupOutput}, /*strict=*/false)); + + TEST_STEP("Report running") + { + score::mw::lifecycle::report_running(); + } + + // The with-dependents case: filesystem_reader asserts on the prepared file and on the setup + // script process being gone, so the ordering is checked there. + TEST_STEP("Activate run target with a terminated-ready component that HAS a dependent") + { + auto result = client.ActivateRunTarget("run_target_reader").Get(stop_token); + EXPECT_TRUE(result.has_value()) << "Activating run_target_reader failed: " << result.error().Message(); + } + + // The no-dependents case: activation must only complete once slow_setup.sh has terminated. + TEST_STEP("Activate run target with a terminated-ready component that has NO dependent") + { + auto result = client.ActivateRunTarget("run_target_slow_setup").Get(stop_token); + EXPECT_TRUE(result.has_value()) << "Activating run_target_slow_setup failed: " << result.error().Message(); + } + + TEST_STEP("Verify slow_setup.sh had terminated before activation completed") + { + EXPECT_TRUE(std::filesystem::exists(kSlowSetupOutput)) + << "run_target_slow_setup reported success while slow_setup.sh was still running: its " + "output file has not been written yet. A run target depending on a terminated-ready " + "component must only become ready once that component's process has actually exited."; + } + + TEST_STEP("Activate run target Off") + { + client.ActivateRunTarget("Off"); + } +} + +int main() +{ + return TestRunner(__FILE__, TerminationBehavior::kWait, TerminationNotification::kTestEnd).RunTests(); +} diff --git a/tests/integration/rt_running_when_process_exits/filesystem_reader.cpp b/tests/integration/rt_running_when_process_exits/filesystem_reader.cpp new file mode 100644 index 000000000..2ce88be4d --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/filesystem_reader.cpp @@ -0,0 +1,117 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "tests/utils/test_helper/test_helper.hpp" +#include + +namespace +{ + +constexpr std::string_view kSetupScriptName = "setup_filesystem.sh"; +constexpr std::string_view kSetupOutputFile = "setup_filesystem_output.txt"; + +/// @brief Returns true if any currently running process was launched from the setup script. +/// +/// This is the programmatic equivalent of inspecting the output of `ps` and looking for the +/// setup_filesystem.sh process: it walks /proc//cmdline and checks whether any argument +/// refers to the setup script. Zombie/reaped processes carry an empty cmdline and are therefore +/// correctly ignored. +bool setup_script_still_running() +{ + for (const auto& entry : std::filesystem::directory_iterator{"/proc"}) + { + if (!entry.is_directory()) + { + continue; + } + + const std::string pid = entry.path().filename().string(); + if (pid.empty() || !std::all_of(pid.begin(), pid.end(), [](unsigned char c) { + return std::isdigit(c); + })) + { + continue; // Not a process directory. + } + + std::ifstream cmdline{entry.path() / "cmdline", std::ios::binary}; + if (!cmdline) + { + continue; // Process may have vanished between listing and reading. + } + + std::stringstream buffer; + buffer << cmdline.rdbuf(); + // cmdline arguments are separated by null bytes; a simple substring search is sufficient. + if (buffer.str().find(kSetupScriptName) != std::string::npos) + { + return true; + } + } + return false; +} + +} // namespace + +// Given a configuration with: +// - A self-terminating component "setup_filesystem_sh" (wrapping setup_filesystem.sh) whose +// ready condition is "Terminated". +// - A component "filesystem_reader" that depends on "setup_filesystem_sh". +// - An initial Run Target "Startup" that depends on "filesystem_reader". +// +// When the Launch Manager activates "Startup", it must first run setup_filesystem.sh to completion +// (the script writes a marker file and exits) before starting filesystem_reader. +TEST(RtRunningWhenProcessExits, FilesystemReader) +{ + ASSERT_TRUE(check_clean({test_end_location})); + + TEST_STEP("Report running") + { + score::mw::lifecycle::report_running(); + } + + TEST_STEP("Read file prepared by setup_filesystem.sh") + { + ASSERT_TRUE(std::filesystem::exists(kSetupOutputFile)) + << "The file prepared by setup_filesystem.sh does not exist; the dependency was not " + "started/finished before filesystem_reader"; + + std::ifstream output{std::filesystem::path{kSetupOutputFile}}; + ASSERT_TRUE(output.is_open()) << "Could not open " << kSetupOutputFile; + std::string content; + std::getline(output, content); + EXPECT_EQ(content, "filesystem is ready") << "Unexpected content in " << kSetupOutputFile; + } + + TEST_STEP("Verify setup_filesystem.sh process has already terminated") + { + EXPECT_FALSE(setup_script_still_running()) + << "The setup_filesystem.sh process is still running; filesystem_reader was started " + "before its dependency terminated"; + } +} + +int main() +{ + // test_end is signalled by control_client_test_driver, which orchestrates the run target switches. + TestRunner runner{__FILE__, TerminationBehavior::kContinue, TerminationNotification::kNone}; + return runner.RunTests(); +} diff --git a/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.json b/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.json new file mode 100644 index 000000000..c251de205 --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.json @@ -0,0 +1,129 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/rt_running_when_process_exits", + "ready_timeout": 2.0, + "shutdown_timeout": 1.0, + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0 + } + }, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib" + }, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_client_test_driver": { + "description": "Drives the test: activates the run targets in sequence and checks that a terminated-ready component with no dependents has actually terminated before its run target reports success.", + "component_properties": { + "binary_name": "control_client_test_driver", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0 + } + } + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": { + "PROCESSIDENTIFIER": "control_client_test_driver" + } + } + }, + "setup_filesystem_sh": { + "description": "Wraps setup_filesystem.sh which prepares the filesystem (here: writes a file) and then terminates. The component becomes ready only once the script process has terminated successfully. It HAS a dependent (filesystem_reader), so its Terminated ready condition is carried on the dependency edge.", + "component_properties": { + "binary_name": "setup_filesystem.sh", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "depends_on": [], + "ready_condition": { + "process_state": "Terminated" + } + } + }, + "filesystem_reader": { + "description": "C++ executable that reads the file prepared by setup_filesystem.sh and verifies that the script process has already terminated.", + "component_properties": { + "binary_name": "filesystem_reader", + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": true + }, + "depends_on": [ + "setup_filesystem_sh" + ], + "ready_condition": { + "process_state": "Running" + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "filesystem_reader" + } + } + }, + "slow_setup_sh": { + "description": "Wraps slow_setup.sh: a self-terminating component whose ready condition is Terminated but which has NO dependent component. It waits briefly before writing its marker file. This reproduces the bug where a run target depending directly on such a component reports success as soon as the process is started, instead of once it has terminated.", + "component_properties": { + "binary_name": "slow_setup.sh", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "depends_on": [], + "ready_condition": { + "process_state": "Terminated" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_client_test_driver" + ] + }, + "run_target_reader": { + "depends_on": [ + "control_client_test_driver", + "filesystem_reader" + ] + }, + "run_target_slow_setup": { + "depends_on": [ + "control_client_test_driver", + "slow_setup_sh" + ] + }, + "Off": { + "depends_on": [] + } + }, + "initial_run_target": "Startup", + "fallback_run_target": { + "depends_on": [] + } +} diff --git a/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.py b/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.py new file mode 100644 index 000000000..beb07de7c --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.py @@ -0,0 +1,65 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +from tests.utils.testing_utils.run_until_file_deployed import run_until_file_deployed +from tests.utils.testing_utils.setup_test import setup_test +from tests.utils.testing_utils.test_results import assert_test_results +from attribute_plugin import add_test_properties + + +@add_test_properties( + partially_verifies=[ + "feat_req__lifecycle__start_named_run_target", + "feat_req__lifecycle__launch_support", + "feat_req__lifecycle__process_state_comm", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_rt_running_when_process_exits( + target, setup_test, assert_test_results, remote_test_dir +): + """ + Objective: Verifies that a Run Target becomes ready only once a self-terminating component's + process has actually exited, both when the terminated-ready component has a dependent and when + it has none. + + A control client (control_client_test_driver) activates two run targets in sequence: + - run_target_reader: filesystem_reader (ready "Running") depends on setup_filesystem_sh + (self-terminating, ready "Terminated"). The terminated-ready + component HAS a dependent, so filesystem_reader finds the prepared + file and confirms the script process is gone. + - run_target_slow_setup: depends directly on slow_setup_sh (self-terminating, ready + "Terminated") which has NO dependent. slow_setup.sh waits briefly + before writing its marker file; the control client verifies that + the marker exists once activation completes, i.e. the run target + only became ready once the process had terminated. + + Expected Behaviour: Both activations complete only after the respective terminated-ready + component's process has exited. + """ + + # launch manager will simply ignore the arguments if run with --//config:use_new_configuration=False. + # the old configuration will be used, which is the default behavior. + # The new configuration will be used if run with --//config:use_new_configuration=True + new_config_path = str(remote_test_dir / "etc/rt_running_when_process_exits.bin") + + run_until_file_deployed( + target=target, + binary_path=str(remote_test_dir / "launch_manager"), + file_path=remote_test_dir.parent / "test_end", + cwd=str(remote_test_dir), + args=["-c", new_config_path], + timeout_s=8.0, + ) + + assert_test_results({"control_client_test_driver.xml", "filesystem_reader.xml"}) diff --git a/tests/integration/rt_running_when_process_exits/setup_filesystem.sh b/tests/integration/rt_running_when_process_exits/setup_filesystem.sh new file mode 100644 index 000000000..bf2d60ac1 --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/setup_filesystem.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# This script stands in for a real filesystem setup step (e.g. mounting +# partitions). Instead of mounting anything it simply writes a marker file that +# a dependent component reads later. It is launched by the Launch Manager as a +# self-terminating component whose ready condition is "Terminated": the Launch +# Manager only considers it ready once this script has exited successfully. + +echo "filesystem is ready" > setup_filesystem_output.txt + +exit 0 diff --git a/tests/integration/rt_running_when_process_exits/slow_setup.sh b/tests/integration/rt_running_when_process_exits/slow_setup.sh new file mode 100644 index 000000000..c0cf957c9 --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/slow_setup.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# This script stands in for a slow, self-terminating filesystem setup step whose +# component has ready condition "Terminated" and NO dependent component. It waits +# for a moment before writing its marker file so that a run target depending +# directly on it can be observed reporting success *before* the process has +# actually terminated (i.e. before the marker file exists) when the deferral of +# graph accounting until termination is not implemented. + +sleep 1 + +echo "slow setup done" > slow_setup_output.txt + +exit 0