diff --git a/score/launch_manager/src/daemon/src/osal/BUILD b/score/launch_manager/src/daemon/src/osal/BUILD index 39a4e849e..b5a576bf7 100644 --- a/score/launch_manager/src/daemon/src/osal/BUILD +++ b/score/launch_manager/src/daemon/src/osal/BUILD @@ -11,6 +11,7 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* load("@rules_cc//cc:defs.bzl", "cc_library") +load("//tests/utils/bazel:unit_test.bzl", "lm_cc_test") cc_library( name = "return_types", @@ -33,6 +34,70 @@ cc_library( deps = [":return_types"], ) +cc_library( + name = "wait_for_file", + srcs = ["details/posix/wait_for_file.cpp"], + hdrs = [ + "return_types.hpp", + "wait_for_file.hpp", + ], + include_prefix = "score/mw/launch_manager/osal", + strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", + visibility = ["//score:__subpackages__"], + deps = [ + ":ifile_waiter", + ":return_types", + "//score/launch_manager/src/daemon/src/configuration:component_config", + "@score_baselibs//score/language/safecpp/string_view:zstring_view", + "@score_baselibs//score/os:errno", + "@score_baselibs//score/os:stat", + "@score_baselibs//score/result:error", + ], +) + +lm_cc_test( + name = "wait_for_file_UT", + srcs = ["details/posix/wait_for_file_UT.cpp"], + deps = [ + ":wait_for_file", + "//score/launch_manager/src/daemon/src/configuration:component_config", + "@googletest//:gtest_main", + "@score_baselibs//score/language/safecpp/string_view:zstring_view", + "@score_baselibs//score/os:errno", + "@score_baselibs//score/os:stat", + "@score_baselibs//score/os/mocklib:stat_mock", + ], +) + +cc_library( + name = "ifile_waiter", + hdrs = [ + "ifile_waiter.hpp", + "return_types.hpp", + ], + include_prefix = "score/mw/launch_manager/osal", + strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", + visibility = ["//score:__subpackages__"], + deps = [ + ":return_types", + "//score/launch_manager/src/daemon/src/configuration:component_config", + "@score_baselibs//score/language/safecpp/string_view:zstring_view", + ], +) + +cc_library( + name = "mock_ifile_waiter", + testonly = True, + hdrs = ["mock_ifile_waiter.hpp"], + include_prefix = "score/mw/launch_manager/osal", + strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", + visibility = ["//score:__subpackages__"], + deps = [ + ":ifile_waiter", + "@googletest//:gtest_main", + ], +) + cc_library( name = "sys_exit", srcs = ["details/posix/sys_exit.cpp"], @@ -127,5 +192,6 @@ cc_library( ":set_affinity", ":set_groups", ":sys_exit", + ":wait_for_file", ], ) diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp new file mode 100644 index 000000000..b205f3760 --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp @@ -0,0 +1,90 @@ +/******************************************************************************** + * 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 "score/os/errno.h" +#include "score/os/stat.h" +#include "score/result/error_code.h" + +#include "score/mw/launch_manager/osal/wait_for_file.hpp" + +namespace score::mw::lifecycle::internal::osal +{ + +OsalReturnType FileWaiter::waitForFile( + score::safecpp::zstring_view path, + configuration::FileExistenceState condition, + std::chrono::milliseconds timeout, + std::chrono::milliseconds poll_interval, + const score::cpp::stop_token& stop_token) const +{ + // note: QNX has a wait_for API, however using the API call we wouldn't be + // able to check the stop_token between stat calls. + + const bool wait_for_existence = (condition == configuration::FileExistenceState::Exists); + const auto deadline = std::chrono::steady_clock::now() + timeout; + + while (true) + { + if (stop_token.stop_requested()) + { + return OsalReturnType::kFail; + } + + score::os::StatBuffer info{}; + + const auto result = stat_os_.stat(path.data(), info); + if (result.has_value()) + { + if (wait_for_existence) + { + return OsalReturnType::kSuccess; + } + } + else + { + switch (result.error().GetOsDependentErrorCode()) + { + case ENOENT: + // treat file or dir not existing as the same + [[fallthrough]]; + case ENOTDIR: + if (!wait_for_existence) + { + return OsalReturnType::kSuccess; + } + break; + case EINTR: + break; // retry + default: + return OsalReturnType::kFail; + } + } + + const auto now = std::chrono::steady_clock::now(); + if (now >= deadline) + { + return OsalReturnType::kTimeout; + } + + // never sleep past the deadline + const auto remaining = deadline - now; + std::this_thread::sleep_for(std::min(poll_interval, remaining)); + } +} + +} // namespace score::mw::lifecycle::internal::osal diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp new file mode 100644 index 000000000..70e094ce3 --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp @@ -0,0 +1,166 @@ +/******************************************************************************** + * 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 "score/os/errno.h" +#include "score/os/mocklib/stat_mock.h" + +#include +#include + +#include + +#include + +#include "score/mw/launch_manager/osal/wait_for_file.hpp" + +using score::mw::lifecycle::internal::configuration::FileExistenceState; +using score::mw::lifecycle::internal::osal::FileWaiter; +using score::mw::lifecycle::internal::osal::OsalReturnType; +using ::testing::_; + +namespace +{ + +constexpr std::chrono::milliseconds kPollInterval{1U}; +constexpr std::chrono::milliseconds kWaitTimeout{2U}; + +class WaitForFileTest : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "explorative-testing"); + } +}; + +TEST_F(WaitForFileTest, FileExists) +{ + RecordProperty( + "Description", "Verify that using FileExistenceState::Exists will return sucess if that stat returns success"); + + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)).WillOnce(testing::Return(score::cpp::expected_blank{})); + EXPECT_EQ( + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), + OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, FileNotExisting) +{ + RecordProperty( + "Description", + "Verify that using FileExistenceState::NotExisting will return sucess if that stat returns ENOTDIR"); + + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillOnce(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOTDIR)))); + EXPECT_EQ( + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::NotExisting, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), + OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, Timeout) +{ + RecordProperty("Description", "Verify that if an error is repeatedly given then the timeout fires."); + + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillRepeatedly(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(EINTR)))); + EXPECT_EQ( + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), + OsalReturnType::kTimeout); +} + +TEST_F(WaitForFileTest, Error) +{ + RecordProperty("Description", "Verify if a unexpected error is recieved from the state call the wait will fail."); + + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillRepeatedly(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(EBADF)))); + EXPECT_EQ( + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), + OsalReturnType::kFail); +} + +TEST_F(WaitForFileTest, NonNullTermindPathFails) +{ + RecordProperty( + "Description", "Verify if using FileExistenceState::Exists the stat is re-polled after the interval."); + + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillOnce(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOTDIR)))) + .WillOnce(testing::Return(score::cpp::expected_blank{})); + + EXPECT_EQ( + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), + OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, StopRequested) +{ + RecordProperty("Description", "Verify that a requested stop causes an early kFail return."); + + score::os::StatMock mock{}; + score::cpp::stop_source stop_source{}; + static_cast(stop_source.request_stop()); + + EXPECT_CALL(mock, stat(_, _, true)).Times(0); + EXPECT_EQ( + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, stop_source.get_token()), + OsalReturnType::kFail); +} + +TEST_F(WaitForFileTest, FileNotExistingViaEnoent) +{ + RecordProperty( + "Description", "Verify that using FileExistenceState::NotExisting will return success if stat returns ENOENT."); + + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillOnce(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOENT)))); + EXPECT_EQ( + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::NotExisting, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), + OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, FileExistsWhileWaitingForNotExisting) +{ + RecordProperty( + "Description", + "Verify that if FileExistenceState::NotExisting is requested but stat still succeeds, the wait " + "is re-polled instead of returning immediately."); + + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillOnce(testing::Return(score::cpp::expected_blank{})) + .WillOnce(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOENT)))); + + EXPECT_EQ( + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::NotExisting, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), + OsalReturnType::kSuccess); +} + +} // namespace diff --git a/score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp b/score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp new file mode 100644 index 000000000..e0bafa096 --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp @@ -0,0 +1,45 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef OSAL_IFILE_WAITER_HPP_INCLUDED +#define OSAL_IFILE_WAITER_HPP_INCLUDED + +#include + +#include "score/language/safecpp/string_view/zstring_view.h" +#include "score/mw/launch_manager/configuration/component_config.hpp" +#include + +#include "return_types.hpp" + +namespace score::mw::lifecycle::internal::osal +{ + +/// @brief Abstraction over wait_for_file() for mocking. +class IFileWaiter +{ + public: + virtual ~IFileWaiter() = default; + + /// @see wait_for_file() for more info. + virtual OsalReturnType waitForFile( + score::safecpp::zstring_view path, + configuration::FileExistenceState condition, + std::chrono::milliseconds timeout, + std::chrono::milliseconds poll_interval, + const score::cpp::stop_token& stop_token) const = 0; +}; + +} // namespace score::mw::lifecycle::internal::osal + +#endif // OSAL_IFILE_WAITER_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/osal/mock_ifile_waiter.hpp b/score/launch_manager/src/daemon/src/osal/mock_ifile_waiter.hpp new file mode 100644 index 000000000..82b3b65f6 --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/mock_ifile_waiter.hpp @@ -0,0 +1,39 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef OSAL_MOCK_IFILE_WAITER_HPP_INCLUDED +#define OSAL_MOCK_IFILE_WAITER_HPP_INCLUDED + +#include "score/mw/launch_manager/osal/ifile_waiter.hpp" +#include + +namespace score::mw::lifecycle::internal::osal +{ + +class MockIFileWaiter : public IFileWaiter +{ + public: + MOCK_METHOD( + OsalReturnType, + waitForFile, + (score::safecpp::zstring_view path, + configuration::FileExistenceState condition, + std::chrono::milliseconds timeout, + std::chrono::milliseconds poll_interval, + const score::cpp::stop_token& stop_token), + (const, override)); +}; + +} // namespace score::mw::lifecycle::internal::osal + +#endif // OSAL_MOCK_IFILE_WAITER_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp new file mode 100644 index 000000000..497913b02 --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp @@ -0,0 +1,59 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef OSAL_WAIT_FOR_FILE_HPP_INCLUDED +#define OSAL_WAIT_FOR_FILE_HPP_INCLUDED + +#include + +#include "score/language/safecpp/string_view/zstring_view.h" +#include "score/mw/launch_manager/configuration/component_config.hpp" +#include "score/mw/launch_manager/osal/ifile_waiter.hpp" +#include "score/os/stat.h" +#include +#include + +#include "return_types.hpp" + +namespace score::mw::lifecycle::internal::osal +{ + +/// @brief IFileWaiter implementation that polls the real filesystem via score::os::Stat. +class FileWaiter final : public IFileWaiter +{ + public: + explicit FileWaiter(const score::os::Stat& stat_os = score::os::Stat::instance()) noexcept : stat_os_(stat_os) + { + } + + /// @brief Blocks until the given path reaches the requested state, the timeout elapses, or a stop is requested. + /// + /// @param path The path to wait for. + /// @param condition The path state to wait for. + /// @param timeout The maximum time to wait for the condition. + /// @param poll_interval The time between two consecutive existence checks. + /// @param stop_token Checked between polls; a requested stop causes an early kFail return. + OsalReturnType waitForFile( + score::safecpp::zstring_view path, + configuration::FileExistenceState condition, + std::chrono::milliseconds timeout, + std::chrono::milliseconds poll_interval, + const score::cpp::stop_token& stop_token) const override; + + private: + const score::os::Stat& stat_os_; +}; + +} // namespace score::mw::lifecycle::internal::osal + +#endif // OSAL_WAIT_FOR_FILE_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/BUILD b/score/launch_manager/src/daemon/src/process_group_manager/BUILD index 707cb5202..fba3d4a79 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/BUILD +++ b/score/launch_manager/src/daemon/src/process_group_manager/BUILD @@ -88,6 +88,7 @@ cc_library( "//score/launch_manager/src/daemon/src/configuration:config", "//score/launch_manager/src/daemon/src/control:control_client_channel", "//score/launch_manager/src/daemon/src/osal:ipc_comms", + "//score/launch_manager/src/daemon/src/osal:wait_for_file", "//score/launch_manager/src/daemon/src/process_group_manager/details:graph", "//score/launch_manager/src/daemon/src/process_group_manager/details:itransition_result_publisher", "//score/launch_manager/src/daemon/src/process_group_manager/details:os_handler", diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD b/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD index de31199ee..d36b17583 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD @@ -143,6 +143,7 @@ cc_library( visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__pkg__"], deps = [ ":safe_process_map", + "//score/launch_manager/src/daemon/src/osal:ifile_waiter", "//score/launch_manager/src/daemon/src/process_group_manager:iprocess", "//score/launch_manager/src/daemon/src/supervision_control_client:isupervision_event_publisher", ], @@ -162,6 +163,7 @@ cc_library( "//score/launch_manager/src/daemon/src/common:alive_interface_path", "//score/launch_manager/src/daemon/src/configuration:component_config", "//score/launch_manager/src/daemon/src/control:control_client_channel", + "//score/launch_manager/src/daemon/src/osal:ifile_waiter", "//score/launch_manager/src/daemon/src/osal:ipc_comms", "//score/launch_manager/src/daemon/src/osal:semaphore", "//score/launch_manager/src/daemon/src/process_group_manager:iprocess", @@ -177,6 +179,7 @@ lm_cc_test( deps = [ ":process_info_node", ":safe_process_map", + "//score/launch_manager/src/daemon/src/osal:mock_ifile_waiter", "//score/launch_manager/src/daemon/src/process_group_manager:mock_iprocess", "//score/launch_manager/src/daemon/src/supervision_control_client:mock_supervision_event_publisher", "@googletest//:gtest_main", diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_handling.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_handling.hpp index 50cfbb37d..eba560a30 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_handling.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_handling.hpp @@ -14,6 +14,7 @@ #ifndef _INCLUDED_PROCESSHANDLING_ #define _INCLUDED_PROCESSHANDLING_ +#include "score/mw/launch_manager/osal/ifile_waiter.hpp" #include "score/mw/launch_manager/process_group_manager/details/safe_process_map.hpp" #include "score/mw/launch_manager/process_group_manager/iprocess.hpp" #include "score/mw/launch_manager/supervision_control_client/isupervision_event_publisher.hpp" @@ -33,6 +34,9 @@ struct ProcessHandling /// @brief Map to store the state of the process. std::shared_ptr process_map_; + + /// @brief Interface used to wait for a FileState ready condition. + osal::IFileWaiter* file_waiter_{nullptr}; }; } // namespace score::mw::lifecycle::internal 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 754e9b1bf..03d85063b 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 @@ -15,6 +15,7 @@ #include "score/launch_manager/src/daemon/src/configuration/component_config.hpp" #include "score/mw/launch_manager/common/alive_interface_path.hpp" #include "score/mw/launch_manager/common/log.hpp" +#include "score/mw/launch_manager/osal/ifile_waiter.hpp" #include "score/mw/launch_manager/osal/ipc_comms.hpp" #include "score/mw/launch_manager/process_group_manager/details/safe_process_map.hpp" #include @@ -32,7 +33,7 @@ ProcessInfoNode::ProcessInfoNode( has_semaphore_(false), process_index_(index), pid_(0), - status_(0), + exit_code_(0), config_(std::move(config)), process_handling_(std::move(process_handling)) { @@ -51,7 +52,7 @@ ProcessInfoNode::ProcessInfoNode( IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecycle::ProcessState new_state) { - ProcessState desired_state{}; + ProcessState desired_state; const auto& ready_condition = config_.component_properties.ready_condition; @@ -70,6 +71,10 @@ IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecy break; } } + else if constexpr (std::is_same_v) + { + desired_state = ProcessState::kRunning; + } }, ready_condition); @@ -162,7 +167,7 @@ IComponent::RequestResult ProcessInfoNode::tryHandleTermination(int32_t process_ { LM_LOG_DEBUG() << "Process" << process_index_ << "pid" << pid_ << "(" << config_.name << ") for node" << this << "terminated with status" << process_status; - status_ = process_status; + exit_code_ = process_status; IComponent::RequestResult res = {IComponent::RequestState::kWaiting}; if (has_semaphore_.exchange(false)) { @@ -189,7 +194,7 @@ IComponent::RequestResult ProcessInfoNode::tryHandleTermination(int32_t process_ else { LM_LOG_WARN() << "unexpected termination of process" << process_index_ << "pid" << pid_ << "(" - << config_.name << ")" << "( status" << status_ << ")"; + << config_.name << ")" << "( status" << exit_code_ << ")"; res = score::cpp::make_unexpected(IComponent::ComponentError::kErrorAfterReady); } } @@ -226,7 +231,7 @@ IComponent::RequestResult ProcessInfoNode::startProcess(score::cpp::stop_token s } pid_ = 0; - status_ = 0; + exit_code_ = 0; error = std::nullopt; static_cast(setState(score::mw::lifecycle::ProcessState::kStarting)); // Cannot fail by design @@ -286,14 +291,55 @@ void ProcessInfoNode::setupControlClientChannel() score::cpp::expected_blank ProcessInfoNode::handleProcessStillStarting( const score::cpp::stop_token& stop_token) { - static_cast(stop_token); // Not yet supported + const bool is_native = + config_.component_properties.application_profile.application_type == configuration::ApplicationType::Native; + + const bool startup_condition_met = std::visit( + [this, is_native, &stop_token](auto&& arg) -> bool { + using T = std::decay_t; + + if constexpr (std::is_same_v) + { + if (is_native) + { + // A native process does not report kRunning, so its status is the only readiness indication. + return exit_code_ == 0; + } + + auto wait_res = process_handling_.process_interface_->waitForkRunning( + sync_, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms)); + return (wait_res == osal::OsalReturnType::kSuccess) && (exit_code_ == 0); + } + // req-id: comp_req__launch_man__path_condition_check + else if constexpr (std::is_same_v) + { + + if (!is_native) + { + // currently we do not support multiple ready conditions so we need + // to ignore the krunning signal. + auto wait_res = process_handling_.process_interface_->ignoreRunning(sync_); + static_cast(wait_res); + } + + const auto wait_res = process_handling_.file_waiter_->waitForFile( + arg.file_path, + arg.state, + std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms), + arg.polling_interval, + stop_token); + + if (wait_res != osal::OsalReturnType::kSuccess) + { + LM_LOG_ERROR() << "Error Waiting for file"; + } + + return (wait_res == osal::OsalReturnType::kSuccess) && (exit_code_ == 0); + } + }, + config_.component_properties.ready_condition); - if (((configuration::ApplicationType::Native == - config_.component_properties.application_profile.application_type) || - (process_handling_.process_interface_->waitForkRunning( - sync_, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms)) == - osal::OsalReturnType::kSuccess)) && - (0 == status_)) + if (startup_condition_met) { handleProcessRunning(); return {}; @@ -304,14 +350,14 @@ score::cpp::expected_blank ProcessInfoNode::handlePr return score::cpp::make_unexpected(ComponentError::kErrorBeforeReady); } - LM_LOG_WARN() << "Got kRunning timeout for process" << process_index_ << "(" << config_.name << ")"; + LM_LOG_WARN() << "Got startup timeout for process" << process_index_ << "(" << config_.name << ")"; terminateProcess(stop_token); return score::cpp::make_unexpected(ComponentError::kActivationTimedOut); } score::cpp::expected_blank ProcessInfoNode::handleProcessAlreadyTerminated() { - if ((0 != status_) || + if ((0 != exit_code_) || (configuration::ApplicationType::Native != config_.component_properties.application_profile.application_type)) { // Error. To get a legal terminated before kRunning the process must be self-terminating, non-reporting @@ -351,8 +397,7 @@ void ProcessInfoNode::handleProcessRunning() { if (configuration::ApplicationType::Native == config_.component_properties.application_profile.application_type) { - LM_LOG_DEBUG() << "Considered kRunning for Non Reporting Process pid" << pid_ << "(" << config_.name - << ") process" << process_index_; + LM_LOG_DEBUG() << "Native process is running " << pid_ << "(" << config_.name << ") process" << process_index_; } else { diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.hpp index 49fc16671..e6fd05cf4 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.hpp @@ -54,7 +54,7 @@ class ProcessInfoNode final : public IComponent has_semaphore_(other.has_semaphore_.load()), process_index_(other.process_index_), pid_(other.pid_), - status_(other.status_.load()), + exit_code_(other.exit_code_.load()), process_state_(other.process_state_.load()), reached_ready_(other.reached_ready_.load()), config_(std::move(other.config_)), @@ -166,7 +166,7 @@ class ProcessInfoNode final : public IComponent osal::ProcessID pid_ = 0; /// @brief The status reported by the operating system when the process terminated - std::atomic status_{0}; + std::atomic exit_code_{0}; /// @brief The current state of the OS process std::atomic process_state_{score::mw::lifecycle::ProcessState::kIdle}; 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 6c176d40a..d32d7fc24 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 @@ -11,6 +11,7 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +#include "score/mw/launch_manager/osal/mock_ifile_waiter.hpp" #include "score/mw/launch_manager/process_group_manager/details/process_info_node.hpp" #include "score/mw/launch_manager/process_group_manager/details/safe_process_map.hpp" #include "score/mw/launch_manager/process_group_manager/mock_iprocess.hpp" @@ -20,6 +21,7 @@ #include #include #include +#include #include #include @@ -71,6 +73,29 @@ class ProcessInfoNodeFixture : public ::testing::Test std::move(config), kProcessIndex, ProcessHandling{mock_publisher_, &mock_processIf_, process_map_}); } + /// @brief Helper method to create a ProcessInfoNode with a FileState ready condition. + std::unique_ptr createFileStateProcessInfoNode( + std::string file_path, + configuration::FileExistenceState state, + configuration::ApplicationType application_type = configuration::ApplicationType::Reporting, + std::chrono::milliseconds ready_timeout = std::chrono::milliseconds{50}, + std::chrono::milliseconds poll_interval = std::chrono::milliseconds{5}) + { + configuration::ComponentConfig config{}; + config.name = "test_process"; + config.component_properties.binary_name = "test_process"; + config.component_properties.application_profile.application_type = application_type; + config.component_properties.ready_condition = + configuration::ReadyCondition{configuration::FileState{std::move(file_path), state, poll_interval}}; + config.deployment_config.ready_timeout_ms = static_cast(ready_timeout.count()); + config.deployment_config.shutdown_timeout_ms = shutdown_timeout_ms_; + + return std::make_unique( + std::move(config), + kProcessIndex, + ProcessHandling{mock_publisher_, &mock_processIf_, process_map_, &mock_file_waiter_}); + } + /// @brief Helper method to create a ProcessInfoNode that is self-terminating. std::unique_ptr createSelfTerminatingProcessInfoNode( configuration::ApplicationType application_type = configuration::ApplicationType::Reporting, @@ -124,6 +149,7 @@ class ProcessInfoNodeFixture : public ::testing::Test score::cpp::stop_source stop_source_{}; std::shared_ptr process_map_{std::make_shared()}; StrictMock mock_processIf_{}; + StrictMock mock_file_waiter_{}; NiceMock mock_publisher_{}; }; @@ -597,3 +623,98 @@ TEST_F(ProcessInfoNodeDeactivationTest, ProcessIgnoresSigterm_ForcedWithSigkill) ASSERT_THAT(node->active(), IsFalse()); ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kIdle)); } + +class ProcessInfoNodeFileStateTest : public ProcessInfoNodeFixture +{ +}; + +TEST_F(ProcessInfoNodeFileStateTest, ConditionAlreadyMet_ReturnsSuccess) +{ + RecordProperty( + "Description", + "A FileState ready condition with Exists that is satisfied lets activate() " + "return kSuccess."); + + auto node = createFileStateProcessInfoNode( + "/ready", + configuration::FileExistenceState::Exists, + configuration::ApplicationType::Reporting, + std::chrono::milliseconds{50}, + std::chrono::milliseconds{5}); + expectSuccessfulProcessLaunch(); + EXPECT_CALL(mock_processIf_, ignoreRunning(_)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL( + mock_file_waiter_, + waitForFile( + _, + Eq(configuration::FileExistenceState::Exists), + Eq(std::chrono::milliseconds{50}), + Eq(std::chrono::milliseconds{5}), + _)) + .WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_publisher_, reportActivation); + + 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::kRunning)); +} + +TEST_F(ProcessInfoNodeFileStateTest, NotExistingCondition_ReturnsSuccess) +{ + RecordProperty( + "Description", + "A FileState ready condition with NotExisting that is satisfied lets activate() " + "return kSuccess."); + + auto node = createFileStateProcessInfoNode("/var/run/gone", configuration::FileExistenceState::NotExisting); + expectSuccessfulProcessLaunch(); + EXPECT_CALL(mock_processIf_, ignoreRunning(_)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_file_waiter_, waitForFile(_, Eq(configuration::FileExistenceState::NotExisting), _, _, _)) + .WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_publisher_, reportActivation); + + 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::kRunning)); +} + +TEST_F(ProcessInfoNodeFileStateTest, NativeApplication_DoesNotIgnoreRunning_ReturnsSuccess) +{ + RecordProperty("Description", "A FileState ready condition with a native process hall not call ignoreRunning."); + + auto node = createFileStateProcessInfoNode( + "/var/run/ready", configuration::FileExistenceState::Exists, configuration::ApplicationType::Native); + expectSuccessfulProcessLaunch(); + EXPECT_CALL(mock_file_waiter_, waitForFile(_, _, _, _, _)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + + 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::kRunning)); +} + +TEST_F(ProcessInfoNodeFileStateTest, WaitForFileTimesOut_ReturnsActivationTimedOut) +{ + RecordProperty( + "Description", + "If waitForFile() times out, activate() returns kActivationTimedOut and the process ends up terminated."); + + auto node = createFileStateProcessInfoNode("/var/run/ready", configuration::FileExistenceState::Exists); + expectSuccessfulProcessLaunch(); + EXPECT_CALL(mock_processIf_, ignoreRunning(_)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_file_waiter_, waitForFile(_, _, _, _, _)).WillOnce(Return(osal::OsalReturnType::kTimeout)); + // Simulate the OS handler reporting the killed process's exit once termination is requested. + expectOsAcknowledgesTermination(node.get()); + EXPECT_CALL(mock_publisher_, reportActivation).Times(0); + + auto result = node->activate(score::cpp::stop_token{}); + + ASSERT_THAT(result.has_value(), IsFalse()); + ASSERT_THAT(result.error(), Eq(IComponent::ComponentError::kActivationTimedOut)); + ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kTerminated)); +} diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp index 86b8e7caa..0722962c6 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp @@ -577,49 +577,66 @@ OsalReturnType ProcessLauncher::waitForTermination(osal::ProcessID& pid, int32_t return result; } +OsalReturnType ProcessLauncher::ignoreRunning(IpcCommsP sync) +{ + if (!sync) + { + LM_LOG_ERROR() << "Invalid shared memory pointer: The shared memory pointer is null."; + return OsalReturnType::kFail; + } + + const auto post_res = sync->reply_sync_.post(); + if (post_res == OsalReturnType::kFail) + { + LM_LOG_ERROR() << "Semaphore post failed"; + return OsalReturnType::kFail; + } + return OsalReturnType::kSuccess; +} + OsalReturnType ProcessLauncher::waitForkRunning(IpcCommsP sync, std::chrono::milliseconds timeout) { OsalReturnType result = OsalReturnType::kSuccess; - if (sync) + if (!sync) { - if ((sync->send_sync_.timedWait(timeout) == OsalReturnType::kFail) || - (sync->reply_sync_.post() == OsalReturnType::kFail)) - { - LM_LOG_ERROR() << "Semaphore timedWait or post failed: Unable to wait or post on semaphores within the " - "specified timeout."; - result = OsalReturnType::kFail; - } - else - { - result = sync->send_sync_.timedWait(std::chrono::milliseconds(100)); - } + LM_LOG_ERROR() << "Invalid shared memory pointer: The shared memory pointer is null."; + return OsalReturnType::kFail; + } - // We are not interested in the result of msync, just whether it worked or not. - // If it did not work, the child process has probably crashed and corrupted the shared memory - // so we should not try to deinitialize the semaphores. - // mincore would be more appropriate here, but is not available on QNX - if (msync(sync.get(), sizeof(IpcCommsSync), MS_ASYNC) == 0) + const auto time_res = sync->send_sync_.timedWait(timeout); + const auto post_res = sync->reply_sync_.post(); + + if ((time_res == OsalReturnType::kFail) || (post_res == OsalReturnType::kFail)) + { + LM_LOG_ERROR() << "Semaphore timedWait or post failed: Unable to wait or post on semaphores within the " + "specified timeout."; + result = OsalReturnType::kFail; + } + else + { + result = sync->send_sync_.timedWait(std::chrono::milliseconds(100)); + } + + // We are not interested in the result of msync, just whether it worked or not. + // If it did not work, the child process has probably crashed and corrupted the shared memory + // so we should not try to deinitialize the semaphores. + // mincore would be more appropriate here, but is not available on QNX + if (msync(sync.get(), sizeof(IpcCommsSync), MS_ASYNC) == 0) + { + if (sync->send_sync_.deinit() != OsalReturnType::kSuccess) { - if (sync->send_sync_.deinit() != OsalReturnType::kSuccess) - { - LM_LOG_WARN() << "Failed to deinitialize send_sync semaphore."; - } - if (sync->reply_sync_.deinit() != OsalReturnType::kSuccess) - { - LM_LOG_WARN() << "Failed to deinitialize reply_sync semaphore."; - } + LM_LOG_WARN() << "Failed to deinitialize send_sync semaphore."; } - else + if (sync->reply_sync_.deinit() != OsalReturnType::kSuccess) { - LM_LOG_WARN() << "Skipping semaphore deinitialization - shared memory region appears invalid: " - << errno_message(errno); + LM_LOG_WARN() << "Failed to deinitialize reply_sync semaphore."; } } else { - LM_LOG_ERROR() << "Invalid shared memory pointer: The shared memory pointer is null."; - result = OsalReturnType::kFail; + LM_LOG_WARN() << "Skipping semaphore deinitialization - shared memory region appears invalid: " + << errno_message(errno); } return result; diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp index d93b361b2..363ca0a33 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp @@ -51,6 +51,9 @@ class ProcessLauncher final : public IProcess /// @see IProcess::waitForkRunning() for details OsalReturnType waitForkRunning(IpcCommsP sync, std::chrono::milliseconds timeout) override; + /// @see IProcess::waitForkRunning() for details + OsalReturnType ignoreRunning(IpcCommsP sync) override; + private: /// @brief Creates shared memory for communication between processes. /// @param[in,out] sync Pointer to a location to store a pointer to a structure containing diff --git a/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp b/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp index 2ec8374da..3831bf3fd 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp @@ -123,6 +123,12 @@ class IProcess /// @return kFail if sync is NULL or a timeout occurs, kSuccess otherwise virtual OsalReturnType waitForkRunning(IpcCommsP sync, std::chrono::milliseconds timeout) = 0; + + /// @brief Ignores a kRunning signal. + /// @param sync The pointer returned from startProcess. + virtual OsalReturnType ignoreRunning(IpcCommsP sync) = 0; + + // virtual OsalReturnType respondToRunning(IpcCommsP sync, std::chrono::milliseconds timeout) = 0; }; } // namespace osal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp b/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp index de14f16a6..d8864ee82 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp @@ -33,6 +33,7 @@ class MockIProcess : public IProcess MOCK_METHOD(OsalReturnType, forceTermination, (ProcessID pid), (override)); MOCK_METHOD(OsalReturnType, waitForTermination, (ProcessID & pid, int32_t& status), (override)); MOCK_METHOD(OsalReturnType, waitForkRunning, (IpcCommsP sync, std::chrono::milliseconds timeout), (override)); + MOCK_METHOD(OsalReturnType, ignoreRunning, (IpcCommsP sync), (override)); }; } // namespace score::mw::lifecycle::internal::osal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.cpp b/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.cpp index ceb4f1ff6..495e9286a 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.cpp @@ -45,6 +45,7 @@ ProcessGroupManager::ProcessGroupManager( std::unique_ptr watchdog) : configuration_(std::move(config)), process_interface_(), + file_waiter_(), process_map_(nullptr), thread_pool_(nullptr), worker_jobs_(nullptr), @@ -210,7 +211,7 @@ bool ProcessGroupManager::initializeProcessGroups() configuration_.components().size() + configuration_.runTargets().size() + 2, configuration_, worker_jobs_, - ProcessHandling{*supervision_control_notifier_.get(), &process_interface_, process_map_}, + ProcessHandling{*supervision_control_notifier_.get(), &process_interface_, process_map_, &file_waiter_}, this); LM_LOG_DEBUG() << "Process group initialized successfully"; diff --git a/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.hpp b/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.hpp index 811cff93a..15977d364 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.hpp @@ -24,6 +24,7 @@ #include "score/mw/launch_manager/common/identifier_hash.hpp" #include "score/mw/launch_manager/configuration/config.hpp" #include "score/mw/launch_manager/control/control_client_channel.hpp" +#include "score/mw/launch_manager/osal/wait_for_file.hpp" #include "score/mw/launch_manager/process_group_manager/details/component_event_queue.hpp" #include "score/mw/launch_manager/process_group_manager/details/graph.hpp" #include "score/mw/launch_manager/process_group_manager/details/itransition_result_publisher.hpp" @@ -264,6 +265,9 @@ class ProcessGroupManager final : public ITransitionResultPublisher /// @brief The process interface object associated with the ProcessGroupManager. osal::ProcessLauncher process_interface_; + /// @brief Waits for FileState ready conditions; injected into ProcessInfoNode via ProcessHandling. + osal::FileWaiter file_waiter_; + /// @brief Shared pointer to the SafeProcessMap object. std::shared_ptr process_map_; diff --git a/tests/integration/ready_conditions/file_state/common/BUILD b/tests/integration/ready_conditions/file_state/common/BUILD new file mode 100644 index 000000000..a097acd5a --- /dev/null +++ b/tests/integration/ready_conditions/file_state/common/BUILD @@ -0,0 +1,26 @@ +# ******************************************************************************* +# 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 = "file_modifier", + srcs = ["file_modifier.cpp"], + visibility = ["//tests/integration/ready_conditions/file_state:__subpackages__"], + deps = [ + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) diff --git a/tests/integration/ready_conditions/file_state/common/file_modifier.cpp b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp new file mode 100644 index 000000000..de1b683bc --- /dev/null +++ b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp @@ -0,0 +1,97 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/test_helper.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum class Operation : std::uint8_t +{ + Create = 0, + Delete +}; +std::string_view g_file_path; +std::chrono::milliseconds g_modify_delay; +Operation g_operation; + +TEST(ModifyFile, ModifyFile) +{ + std::this_thread::sleep_for(g_modify_delay); + switch (g_operation) + { + case (Operation::Create): + ASSERT_TRUE(touch_file(g_file_path)); + std::cout << "created file " << g_file_path << "By " << ::getpid() << std::endl; + break; + case (Operation::Delete): + std::error_code error{}; + ASSERT_TRUE(std::filesystem::remove(g_file_path, error)) + << "Could not remove file " << g_file_path << ": " + << (error ? error.message() : "the file did not exist"); + std::cout << "deleted file " << g_file_path << "By " << ::getpid() << std::endl; + break; + } + std::cout << "Operation complete!" << std::endl; +} + +int main(int argc, char** argv) +{ + std::cout << "Starting" << std::endl; + if (argc != 5) + { + std::cerr << "USAGE:" << argv[0] + << "(action [delete|create]) (file path) (milliseconds to wait before doing the operation) " + "(shall_report [0|1])" + << std::endl; + return EXIT_FAILURE; + } + + const std::string_view action{argv[1]}; + if (action == "create") + { + g_operation = Operation::Create; + } + else if (action == "delete") + { + g_operation = Operation::Delete; + } + else + { + std::cerr << "Program has to be called either file_creator or file_deletor" << std::endl; + return EXIT_FAILURE; + } + + g_file_path = std::string_view{argv[2]}; + g_modify_delay = std::chrono::milliseconds{std::stoi(argv[3])}; + + std::string xml_result{argv[0]}; + + const std::string_view shall_report{argv[4]}; + if (shall_report == "report") + { + std::cout << "REPORTING RUNNING from " << ::getpid() << std::endl; + score::mw::lifecycle::report_running(); + std::cout << "REPORTED!" << ::getpid() << std::endl; + xml_result.append("_reporting"); + } + + return TestRunner(xml_result).RunTests(); +} diff --git a/tests/integration/ready_conditions/file_state/exists/BUILD b/tests/integration/ready_conditions/file_state/exists/BUILD new file mode 100644 index 000000000..eec21764e --- /dev/null +++ b/tests/integration/ready_conditions/file_state/exists/BUILD @@ -0,0 +1,36 @@ +# ******************************************************************************* +# 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 = "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 = "ready_condition_file", + srcs = ["ready_condition_file.py"], + binaries = [ + "//tests/integration/ready_conditions/file_state/common:file_modifier", + ":control_client_test_driver", + "//score/launch_manager", + ], + config = ":ready_condition_file.json", +) diff --git a/tests/integration/ready_conditions/file_state/exists/control_client_test_driver.cpp b/tests/integration/ready_conditions/file_state/exists/control_client_test_driver.cpp new file mode 100644 index 000000000..888c40a6f --- /dev/null +++ b/tests/integration/ready_conditions/file_state/exists/control_client_test_driver.cpp @@ -0,0 +1,53 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/test_helper.hpp" +#include +#include + +TEST(FileStateExist, ControlClientTestDriver) +{ + score::mw::lifecycle::ControlClient client; + + ASSERT_TRUE(check_clean({test_end_location, fallback_file})); + + TEST_STEP("Report kRunning from ControlClientTestDriver") + { + score::mw::lifecycle::report_running(); + } + + TEST_STEP("Activate RunTarget that works") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("working").Get(stop_token); + EXPECT_TRUE(result.has_value()); + } + + TEST_STEP("Activate RunTarget that times out") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("timeout").Get(stop_token); + EXPECT_FALSE(result.has_value()) << "Activation should timeout and error"; + } + + TEST_STEP("Activate RunTarget Off") + { + client.ActivateRunTarget("Off"); + } +} + +int main() +{ + return TestRunner(__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd).RunTests(); +} diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json new file mode 100644 index 000000000..06dc49145 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json @@ -0,0 +1,134 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/ready_condition_file", + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + }, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Native", + "is_self_terminating": false + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_client_test_driver": { + "component_properties": { + "binary_name": "control_client_test_driver", + "ready_condition": { + "process_state": "Running" + }, + "application_profile": { + "application_type": "State_Manager", + "is_self_terminating": false, + "alive_supervision": { + "min_indications": 0 + } + } + } + }, + "file_creating_component": { + "component_properties": { + "binary_name": "file_modifier", + "process_arguments": [ + "create", "/tmp/tests/ready_condition_file/ready_file", "0", "native" + ], + "ready_condition": { + "file_state": { + "file_path": "/tmp/tests/ready_condition_file/ready_file", + "state": "Exists", + "polling_interval": 0.05 + } + } + } + }, + "file_creating_component_reporting": { + "component_properties": { + "binary_name": "file_modifier", + "process_arguments": [ + "create", "/tmp/tests/ready_condition_file/ready_file_reporting", "0", "report" + ], + "application_profile": { + "application_type": "Reporting" + }, + "ready_condition": { + "file_state": { + "file_path": "/tmp/tests/ready_condition_file/ready_file_reporting", + "state": "Exists", + "polling_interval": 0.05 + } + } + } + }, + "file_creating_component_timeout": { + "component_properties": { + "binary_name": "file_modifier", + "process_arguments": [ + "create", "/tmp/tests/ready_condition_file/ready_file_2", "1500", "native" + ], + "ready_condition": { + "file_state": { + "file_path": "/tmp/tests/ready_condition_file/ready_file_2", + "state": "Exists", + "polling_interval": 0.1 + } + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_client_test_driver" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "working": { + "depends_on": [ + "file_creating_component", + "file_creating_component_reporting", + "control_client_test_driver" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "timeout": { + "depends_on": [ + "file_creating_component_timeout", + "control_client_test_driver" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Startup", + "fallback_run_target": { + "depends_on": ["control_client_test_driver"] + } +} diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py new file mode 100644 index 000000000..7d11879e7 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py @@ -0,0 +1,52 @@ +# ******************************************************************************* +# 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=["comp_req__launch_man__path_condition_check"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_ready_condition_file(target, setup_test, assert_test_results, remote_test_dir): + """ + Objective: Verifies that a component with a file_state ready condition only + reaches its ready state once the configured file exists. + + The initial run target contains a component that touches its ready + condition file after a delay. + + Expected Behaviour: The launch manager polls for the file and only starts + the dependent component after the file has been created. + """ + + config_path = str(remote_test_dir / "etc/ready_condition_file.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", config_path], + timeout_s=3.0, + ) + + assert_test_results( + { + "control_client_test_driver.xml", + "file_modifier.xml", + "file_modifier_reporting.xml", + } + ) diff --git a/tests/integration/ready_conditions/file_state/not_existing/BUILD b/tests/integration/ready_conditions/file_state/not_existing/BUILD new file mode 100644 index 000000000..29ddd8d53 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/not_existing/BUILD @@ -0,0 +1,36 @@ +# ******************************************************************************* +# 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 = "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 = "ready_condition_file_not_exists", + srcs = ["ready_condition_file_not_existing.py"], + binaries = [ + "//tests/integration/ready_conditions/file_state/common:file_modifier", + ":control_client_test_driver", + "//score/launch_manager", + ], + config = ":ready_condition_file_not_existing.json", +) diff --git a/tests/integration/ready_conditions/file_state/not_existing/control_client_test_driver.cpp b/tests/integration/ready_conditions/file_state/not_existing/control_client_test_driver.cpp new file mode 100644 index 000000000..33f510e52 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/not_existing/control_client_test_driver.cpp @@ -0,0 +1,53 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/test_helper.hpp" +#include +#include + +TEST(FileStateNotExistng, ControlClientTestDriver) +{ + score::mw::lifecycle::ControlClient client; + + ASSERT_TRUE(check_clean({test_end_location, fallback_file})); + + TEST_STEP("Report kRunning from ControlClientTestDriver") + { + score::mw::lifecycle::report_running(); + } + + TEST_STEP("Activate RunTarget that works") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("working").Get(stop_token); + EXPECT_TRUE(result.has_value()); + } + + TEST_STEP("Activate RunTarget that times out") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("timeout").Get(stop_token); + EXPECT_FALSE(result.has_value()) << "Activation should timeout and error"; + } + + TEST_STEP("Activate RunTarget Off") + { + client.ActivateRunTarget("Off"); + } +} + +int main() +{ + return TestRunner(__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd).RunTests(); +} diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json new file mode 100644 index 000000000..d3ac09185 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json @@ -0,0 +1,137 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/ready_condition_file_not_exists", + "ready_timeout": 0.1, + "shutdown_timeout": 1.0, + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + }, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Native", + "is_self_terminating": false + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_client_test_driver": { + "component_properties": { + "binary_name": "control_client_test_driver", + "application_profile": { + "application_type": "State_Manager" + } + } + }, + "file_deleting_component": { + "component_properties": { + "binary_name": "file_modifier", + "process_arguments": [ + "delete", "/tmp/tests/ready_condition_file_not_exists/vanishing_file", "0", "native" + ], + "application_profile": { + "application_type": "Native" + }, + "ready_condition": { + "file_state": { + "file_path": "/tmp/tests/ready_condition_file_not_exists/vanishing_file", + "state": "NotExisting", + "polling_interval": 0.1 + } + } + } + }, + "file_deleting_component_reporting": { + "component_properties": { + "binary_name": "file_modifier", + "process_arguments": [ + "delete", "/tmp/tests/ready_condition_file_not_exists/vanishing_file_report", "0", "report" + ], + "application_profile": { + "application_type": "Reporting" + }, + "ready_condition": { + "file_state": { + "file_path": "/tmp/tests/ready_condition_file_not_exists/vanishing_file_report", + "state": "NotExisting", + "polling_interval": 0.1 + } + } + } + }, + "file_deleting_component_timeout": { + "component_properties": { + "binary_name": "file_modifier", + "process_arguments": [ + "delete", "/tmp/tests/ready_condition_file_not_exists/vanishing_file_2", "500", "native" + ], + "ready_condition": { + "file_state": { + "file_path": "/tmp/tests/ready_condition_file_not_exists/vanishing_file_2", + "state": "NotExisting", + "polling_interval": 0.1 + } + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_client_test_driver" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "working": { + "depends_on": [ + "file_deleting_component", + "control_client_test_driver" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "timeout": { + "depends_on": [ + "file_deleting_component_timeout", + "control_client_test_driver" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [] + } +} diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py new file mode 100644 index 000000000..d8c63648a --- /dev/null +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py @@ -0,0 +1,57 @@ +# ******************************************************************************* +# 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=["comp_req__launch_man__path_condition_check"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_ready_condition_file_not_existing( + target, setup_test, assert_test_results, remote_test_dir +): + """ + Objective: Verifies that a component with a NotExisting file_state ready + condition only reaches its ready state once the configured file is gone. + + The initial run target contains a component that removes its ready + condition file after a delay, and a second component depending on it. + + Expected Behaviour: The launch manager polls for the file and only starts + the dependent component after the file has been removed. + """ + + config_path = str(remote_test_dir / "etc/ready_condition_file_not_existing.bin") + vanishing_file = str(remote_test_dir / "vanishing_file") + + # The file has to be there when the launch manager starts polling, otherwise the ready + # condition is satisfied right away and the test would pass without waiting for anything. + res, stdout = target.execute(f"touch {vanishing_file}") + res, stdout = target.execute(f"touch {vanishing_file}_report") + res, stdout = target.execute(f"touch {vanishing_file}_2") + assert res == 0, stdout + + 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", config_path], + timeout_s=3.0, + ) + + assert_test_results({"control_client_test_driver.xml", "file_modifier.xml"})