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 77f3017404..ac11fef575 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 @@ -14,6 +14,37 @@ load("@rules_cc//cc:defs.bzl", "cc_library") load("//tests/utils/bazel:unit_test.bzl", "lm_cc_test") +cc_library( + name = "component", + srcs = ["component.cpp"], + hdrs = ["component.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + ":icomponent", + "//score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action:iforce_stop_action", + "//score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition:iready_condition", + "//score/launch_manager/src/daemon/src/process_group_manager/details/start_action:istart_action", + "//score/launch_manager/src/daemon/src/process_group_manager/details/stop_action:istop_action", + "@score_baselibs//score/language/futurecpp", + ], +) + +lm_cc_test( + name = "component_UT", + srcs = ["component_UT.cpp"], + deps = [ + ":component", + ":mock_component", + "//score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action:mock_force_stop_action", + "//score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition:mock_ready_condition", + "//score/launch_manager/src/daemon/src/process_group_manager/details/start_action:mock_start_action", + "//score/launch_manager/src/daemon/src/process_group_manager/details/stop_action:mock_stop_action", + "@googletest//:gtest_main", + ], +) + cc_library( name = "component_task", hdrs = ["component_task.hpp"], @@ -49,6 +80,16 @@ cc_library( ], ) +cc_library( + name = "handle", + srcs = ["handle.cpp"], + hdrs = ["handle.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = ["//score/launch_manager/src/daemon/src/osal:ipc_comms"], +) + cc_library( name = "mock_component", testonly = True, @@ -393,7 +434,7 @@ cc_library( hdrs = ["process_launcher.hpp"], include_prefix = "score/mw/launch_manager/process_group_manager/details", strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", - visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__pkg__"], + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], deps = [ "//score/launch_manager/src/daemon/src/common:log", "//score/launch_manager/src/daemon/src/common:signal_safe_log", diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/component.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/component.cpp new file mode 100644 index 0000000000..6c692fcddc --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/component.cpp @@ -0,0 +1,179 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/component.hpp" + +namespace score::mw::lifecycle::internal +{ + +Component::Component( + const IStartAction& start_action, + const IStopAction& stop_action, + const IForceStopAction& force_stop_action, + const IReadyCondition& ready_condition, + bool self_terminating, + IdentifierHash identifier) + : start_action_(start_action), + stop_action_(stop_action), + force_stop_action_(force_stop_action), + ready_condition_(ready_condition), + self_terminating_(self_terminating), + identifier_(identifier), + state_(TerminatedState{}) +{ +} + +IComponent::RequestResult Component::activate(cpp::stop_token stop_token) +{ + std::unique_lock lock{state_mutex_}; + + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + std::holds_alternative(state_), "Cannot activate component in a running state"); + + const Result start_result = start_action_.start(stop_token); + if (!start_result.has_value()) + { + state_ = FaultState{}; + return cpp::make_unexpected(ComponentError::kErrorBeforeReady); + } + const Handle& handle = start_result.value(); + + cpp::stop_source stop_activation{}; + const cpp::stop_callback forward_stop{stop_token, [&stop_activation] { + // This can fail if stop is requested from both places. + static_cast(stop_activation.request_stop()); + }}; + + state_ = StartingState{handle, stop_activation}; + + // Wait for the ready condition. The state is unlocked so that `deactivate` or + // `tryHandleTermination` are callable while we are waiting. + lock.unlock(); + if (!ready_condition_.wait(stop_activation.get_token(), handle).has_value()) + { + return cpp::make_unexpected(ComponentError::kErrorBeforeReady); + } + lock.lock(); + + // If `deactivate` or `tryHandleTermination` was called during the wait then + // activation fails. + if (stop_activation.stop_requested()) + { + return cpp::make_unexpected(ComponentError::kErrorBeforeReady); + } + + // The state could change while the lock was released, so we have to check. + if (const StartingState* const starting = std::get_if(&state_)) + { + state_ = ReadyState{starting->handle_}; + return RequestState::kSuccess; + } + + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE("If stop was not requested, then state must still be StartingState"); +} + +IComponent::RequestResult Component::deactivate(cpp::stop_token stop_token) +{ + const std::lock_guard lock{state_mutex_}; + + StartingState* const starting = std::get_if(&state_); + const ReadyState* const ready = std::get_if(&state_); + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + (starting != nullptr) || (ready != nullptr), "Cannot deactivate component in a non-running state"); + const Handle& handle = (starting != nullptr) ? starting->handle_ : ready->handle_; + + // If the component is still waiting for ready conditions, cancel that. + if (starting != nullptr) + { + static_cast(starting->stop_activation_.request_stop()); + } + + const Result result = stop_action_.stop(stop_token, handle); + if (!result.has_value()) + { + state_ = FaultState{handle}; + return cpp::make_unexpected(ComponentError::kErrorAfterReady); + } + + switch (*result.value()) + { + // Wait for `tryHandleTermination` to tell us the termination is done. + case RequestState::kWaiting: + state_ = TerminatingState{handle}; + return RequestState::kWaiting; + + // Already terminated, jump straight to the final state. + case RequestState::kSuccess: + state_ = TerminatedState{}; + return RequestState::kSuccess; + } + + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE("Every possible RequestState should be handled"); +} + +IComponent::RequestResult Component::tryHandleTermination(int32_t status) +{ + const std::lock_guard lock{state_mutex_}; + + // If we are waiting for termination, mark it as done. We do not care + // about the exit code. + if (std::holds_alternative(state_)) + { + state_ = TerminatedState{}; + return RequestState::kWaiting; + } + + StartingState* const starting = std::get_if(&state_); + ReadyState* const ready = std::get_if(&state_); + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + (starting != nullptr) || (ready != nullptr), "Cannot handle termination in a non-running state"); + Handle& handle = (starting != nullptr) ? starting->handle_ : ready->handle_; + + // Invalidate the resource to prevent acting on something which is already gone. + invalidate(handle); + + // Certain ready conditions can accept terminations. + if (starting != nullptr && ready_condition_.tryHandleTermination(handle, status)) + { + return RequestState::kWaiting; + } + + // Self-terminating components can accept terminations. + if (self_terminating_ && status == 0) + { + return RequestState::kWaiting; + } + + // If none of the above cases apply, then termination is unexpected. + if (starting != nullptr) + { + // Interrupt the worker thread waiting in activate(). + static_cast(starting->stop_activation_.request_stop()); + } + + state_ = FaultState{}; + return cpp::make_unexpected(ComponentError::kErrorAfterReady); +} + +IdentifierHash Component::getIdentifier() const +{ + return identifier_; +} + +bool Component::active() const +{ + const std::lock_guard lock{state_mutex_}; + return std::holds_alternative(state_); +} + +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/component.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/component.hpp new file mode 100644 index 0000000000..9f503fa644 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/component.hpp @@ -0,0 +1,128 @@ +/******************************************************************************** + * 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 SCORE_LCM_COMPONENT_HPP_INCLUDED +#define SCORE_LCM_COMPONENT_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/force_stop_action/iforce_stop_action.hpp" +#include "score/mw/launch_manager/process_group_manager/details/icomponent.hpp" +#include "score/mw/launch_manager/process_group_manager/details/ready_condition/iready_condition.hpp" +#include "score/mw/launch_manager/process_group_manager/details/start_action/istart_action.hpp" +#include "score/mw/launch_manager/process_group_manager/details/stop_action/istop_action.hpp" +#include +#include + +namespace score::mw::lifecycle::internal +{ + +/// @brief Manages and tracks the state of a resource. +class Component final : public IComponent +{ + public: + /// @brief Creates a new component from the given actions. + /// @param start_action How to start the resource. + /// @param stop_action How to gracefully stop the resource. + /// @param force_stop_action How to forcefully stop the resource. + /// @param ready_condition How to decide when the resource has finished its startup. + /// @param self_terminating Whether the resource is allowed to terminate on its own. + /// @param identifier Name of the component. + Component( + const IStartAction& start_action, + const IStopAction& stop_action, + const IForceStopAction& force_stop_action, + const IReadyCondition& ready_condition, + bool self_terminating = false, + IdentifierHash identifier = IdentifierHash{}); + + /// @brief Start the resource and set the component to active. + /// @param stop_token Token which can be used to interrupt the activation. + /// @return Whether the component activation was successful, is waiting for another + /// thread, or an error was encountered. + RequestResult activate(cpp::stop_token stop_token) override; + + /// @brief Stop the resource and set the component to inactive. + /// @param stop_token Token which can be used to interrupt the deactivation. + /// @return Whether the component deactivation was successful, is waiting for another + /// thread, or an error was encountered. + RequestResult deactivate(cpp::stop_token stop_token) override; + + /// @brief Notify the component that a POSIX process has terminated. + /// @param status Exit code of the process. + /// @return Whether the component activation was successful, is waiting for another + /// thread, or an error was encountered. + RequestResult tryHandleTermination(int32_t status) override; + + /// @brief Return the name of the component. + /// @return The name of the component. + [[nodiscard]] IdentifierHash getIdentifier() const override; + + /// @brief Return whether the component is active. + /// @return Whether the component is active. + [[nodiscard]] bool active() const override; + + private: + /// @brief How to start the resource. + const IStartAction& start_action_; + + /// @brief How to gracefully stop the resource. + const IStopAction& stop_action_; + + /// @brief How to forcefully stop the resource. + const IForceStopAction& force_stop_action_; + + /// @brief How to decide when the resource has finished its startup. + const IReadyCondition& ready_condition_; + + /// @brief Whether the resource is allowed to terminate on its own. + const bool self_terminating_; + + /// @brief Name of the component. + const IdentifierHash identifier_; + + struct TerminatedState final + { + }; + + struct StartingState final + { + Handle handle_; + cpp::stop_source stop_activation_; + }; + + struct ReadyState final + { + Handle handle_; + }; + + struct TerminatingState final + { + Handle handle_; + }; + + struct FaultState final + { + Handle handle_; + }; + + using State = std::variant; + + /// @brief Protects the state from concurrent modifications. + mutable std::mutex state_mutex_; + + /// @brief The current state of the component. + State state_; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_COMPONENT_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/component_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/component_UT.cpp new file mode 100644 index 0000000000..bc43a0c760 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/component_UT.cpp @@ -0,0 +1,471 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/component.hpp" +#include "score/mw/launch_manager/process_group_manager/details/force_stop_action/mock_force_stop_action.hpp" +#include "score/mw/launch_manager/process_group_manager/details/ready_condition/mock_ready_condition.hpp" +#include "score/mw/launch_manager/process_group_manager/details/start_action/mock_start_action.hpp" +#include "score/mw/launch_manager/process_group_manager/details/stop_action/mock_stop_action.hpp" +#include +#include +#include +#include + +namespace score::mw::lifecycle::internal +{ +namespace +{ + +using namespace ::testing; + +const ProcessHandle mock_handle = ProcessHandle{42}; + +const cpp::stop_source mock_stop_source; +const cpp::stop_token mock_stop_token = mock_stop_source.get_token(); + +void expect_mock_handle(const Handle handle) +{ + EXPECT_EQ(std::get(handle).pid, mock_handle.pid); +} + +IComponent::RequestResult terminateFromOtherThread(Component& component, const int32_t status) +{ + // Termination notifications come from another thread in reality, so + // replacate it in the test to catch concurrency problems. + + IComponent::RequestResult result; + std::thread termination_thread([&component, &result, status]() { + result = component.tryHandleTermination(status); + }); + termination_thread.join(); + return result; +} + +TEST(ComponentTest, StartSucceeds) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + const auto result = component.activate(cpp::stop_token{}); + + EXPECT_TRUE(result.has_value()); +} + +TEST(ComponentTest, StartActionCalled) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + EXPECT_CALL(start_action, start(_)).WillOnce(Return(Result{mock_handle})); + + static_cast(component.activate(mock_stop_token)); +} + +TEST(ComponentTest, StopActionCalled) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + EXPECT_CALL(stop_action, stop(_, _)) + .WillOnce(DoAll(WithArg<1>(Invoke(expect_mock_handle)), Return(IComponent::RequestState::kSuccess))); + + static_cast(component.activate(cpp::stop_token{})); + component.deactivate(mock_stop_token); +} + +TEST(ComponentTest, DeactivateReturnsWaitingWhenStopActionReturnsWaiting) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + ON_CALL(stop_action, stop(_, _)).WillByDefault(Return(IComponent::RequestState::kWaiting)); + + static_cast(component.activate(cpp::stop_token{})); + const auto result = component.deactivate(mock_stop_token); + + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(result.value(), IComponent::RequestState::kWaiting); +} + +TEST(ComponentTest, DeactivateReturnsSuccessWhenStopActionReturnsSuccess) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + ON_CALL(stop_action, stop(_, _)).WillByDefault(Return(IComponent::RequestState::kSuccess)); + + static_cast(component.activate(cpp::stop_token{})); + const auto result = component.deactivate(mock_stop_token); + + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(result.value(), IComponent::RequestState::kSuccess); +} + +TEST(ComponentTest, ReadyConditionCalled) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + EXPECT_CALL(ready_condition, wait(_, _)) + .WillOnce(DoAll(WithArg<1>(Invoke(expect_mock_handle)), Return(Result{}))); + + static_cast(component.activate(mock_stop_token)); +} + +TEST(ComponentTest, TerminationDuringStartupInterruptsActivation) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + EXPECT_CALL(ready_condition, wait(_, _)).WillOnce(Invoke([&component](cpp::stop_token token, const Handle) { + static_cast(terminateFromOtherThread(component, 1)); + + // As a result of the termination, the ready condition is stopped. + EXPECT_TRUE(token.stop_requested()); + return Result{}; + })); + + const auto result = component.activate(cpp::stop_token{}); + + EXPECT_FALSE(result.has_value()); +} + +TEST(ComponentTest, StartSetsActive) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + static_cast(component.activate(cpp::stop_token{})); + + EXPECT_TRUE(component.active()); +} + +TEST(ComponentTest, StopClearsActive) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + static_cast(component.activate(cpp::stop_token{})); + component.deactivate(cpp::stop_token{}); + + EXPECT_FALSE(component.active()); +} + +TEST(ComponentTest, CannotDeactivateInTerminatedState) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + EXPECT_DEATH( + { static_cast(component.deactivate(cpp::stop_token{})); }, + "Cannot deactivate component in a non-running state"); +} + +TEST(ComponentTest, CannotActivateInReadyState) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + + static_cast(component.activate(cpp::stop_token{})); + + EXPECT_DEATH( + { static_cast(component.activate(cpp::stop_token{})); }, "Cannot activate component in a running state"); +} + +TEST(ComponentTest, CannotActivateInTerminatingState) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + ON_CALL(stop_action, stop(_, _)).WillByDefault(Return(IComponent::RequestState::kWaiting)); + + static_cast(component.activate(cpp::stop_token{})); + static_cast(component.deactivate(cpp::stop_token{})); + + EXPECT_DEATH( + { static_cast(component.activate(cpp::stop_token{})); }, "Cannot activate component in a running state"); +} + +TEST(ComponentTest, CannotDeactivateInTerminatingState) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + ON_CALL(stop_action, stop(_, _)).WillByDefault(Return(IComponent::RequestState::kWaiting)); + + static_cast(component.activate(cpp::stop_token{})); + static_cast(component.deactivate(cpp::stop_token{})); + + EXPECT_DEATH( + { static_cast(component.deactivate(cpp::stop_token{})); }, + "Cannot deactivate component in a non-running state"); +} + +TEST(ComponentTest, CannotActivateInFaultState) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + + static_cast(component.activate(cpp::stop_token{})); + component.tryHandleTermination(1); + + EXPECT_DEATH( + { static_cast(component.activate(cpp::stop_token{})); }, "Cannot activate component in a running state"); +} + +TEST(ComponentTest, CannotDeactivateInFaultState) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + + static_cast(component.activate(cpp::stop_token{})); + component.tryHandleTermination(1); + + EXPECT_DEATH( + { static_cast(component.deactivate(cpp::stop_token{})); }, + "Cannot deactivate component in a non-running state"); +} + +TEST(ComponentTest, SelfTerminatingExitZeroInReadyStateIsWaiting) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition, true); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + + static_cast(component.activate(cpp::stop_token{})); + const auto result = component.tryHandleTermination(0); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), IComponent::RequestState::kWaiting); +} + +TEST(ComponentTest, SelfTerminatingExitZeroInReadyStateIsActive) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition, true); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + + static_cast(component.activate(cpp::stop_token{})); + static_cast(component.tryHandleTermination(0)); + + EXPECT_TRUE(component.active()); +} + +TEST(ComponentTest, SelfTerminatingExitNonZeroInReadyStateIsError) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition, true); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + + static_cast(component.activate(cpp::stop_token{})); + const auto result = component.tryHandleTermination(1); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), IComponent::ComponentError::kErrorAfterReady); +} + +TEST(ComponentTest, SelfTerminatingExitNonZeroInReadyStateIsNotActive) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition, true); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + + static_cast(component.activate(cpp::stop_token{})); + static_cast(component.tryHandleTermination(1)); + + EXPECT_FALSE(component.active()); +} + +TEST(ComponentTest, SelfTerminatingExitZeroInStartingStateIsWaiting) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition, true); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + + IComponent::RequestResult result; + EXPECT_CALL(ready_condition, wait(_, _)).WillOnce(Invoke([&](cpp::stop_token, const Handle) { + result = terminateFromOtherThread(component, 0); + return Result{}; + })); + + static_cast(component.activate(cpp::stop_token{})); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), IComponent::RequestState::kWaiting); +} + +TEST(ComponentTest, SelfTerminatingExitZeroInStartingStateIsActive) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition, true); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + EXPECT_CALL(ready_condition, wait(_, _)).WillOnce(Invoke([&](cpp::stop_token, const Handle) { + static_cast(terminateFromOtherThread(component, 0)); + return Result{}; + })); + + static_cast(component.activate(cpp::stop_token{})); + + EXPECT_TRUE(component.active()); +} + +TEST(ComponentTest, SelfTerminatingExitNonZeroInStartingStateIsError) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition, true); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + + IComponent::RequestResult result; + EXPECT_CALL(ready_condition, wait(_, _)).WillOnce(Invoke([&](cpp::stop_token, const Handle) { + result = terminateFromOtherThread(component, 1); + return Result{}; + })); + + static_cast(component.activate(cpp::stop_token{})); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), IComponent::ComponentError::kErrorAfterReady); +} + +TEST(ComponentTest, SelfTerminatingExitNonZeroInStartingStateIsNotActive) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition, true); + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + EXPECT_CALL(ready_condition, wait(_, _)).WillOnce(Invoke([&](cpp::stop_token, const Handle) { + static_cast(terminateFromOtherThread(component, 1)); + return Result{}; + })); + + static_cast(component.activate(cpp::stop_token{})); + + EXPECT_FALSE(component.active()); +} + +TEST(ComponentTest, SettingBothStopTokens) +{ + MockStartAction start_action; + MockStopAction stop_action; + MockForceStopAction force_stop_action; + MockReadyCondition ready_condition; + Component component(start_action, stop_action, force_stop_action, ready_condition); + cpp::stop_source stop_source{}; + + ON_CALL(start_action, start(_)).WillByDefault(Return(Result{mock_handle})); + EXPECT_CALL(ready_condition, wait(_, _)).WillOnce(Invoke([&](cpp::stop_token token, const Handle) { + static_cast(terminateFromOtherThread(component, 1)); + EXPECT_TRUE(token.stop_requested()); + + static_cast(stop_source.request_stop()); + EXPECT_TRUE(token.stop_requested()); + + return Result{}; + })); + + const auto result = component.activate(stop_source.get_token()); + + EXPECT_FALSE(result.has_value()); +} + +} // namespace +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/BUILD b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/BUILD new file mode 100644 index 0000000000..8d6e56e29a --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/BUILD @@ -0,0 +1,86 @@ +# ******************************************************************************* +# 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("//tests/utils/bazel:unit_test.bzl", "lm_cc_test") + +cc_library( + name = "iforce_stop_action", + hdrs = ["iforce_stop_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + "//score/launch_manager/src/daemon/src/process_group_manager/details:handle", + "//score/launch_manager/src/daemon/src/process_group_manager/details:icomponent", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/result", + ], +) + +cc_library( + name = "mock_force_stop_action", + testonly = True, + hdrs = ["mock_force_stop_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + ":iforce_stop_action", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "empty_force_stop_action", + srcs = ["empty_force_stop_action.cpp"], + hdrs = ["empty_force_stop_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details/force_stop_action", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [":iforce_stop_action"], +) + +lm_cc_test( + name = "empty_force_stop_action_UT", + srcs = ["empty_force_stop_action_UT.cpp"], + deps = [ + ":empty_force_stop_action", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "process_force_stop_action", + srcs = ["process_force_stop_action.cpp"], + hdrs = ["process_force_stop_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details/force_stop_action", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + ":iforce_stop_action", + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/osal:return_types", + "//score/launch_manager/src/daemon/src/process_group_manager:iprocess", + ], +) + +lm_cc_test( + name = "process_force_stop_action_UT", + srcs = ["process_force_stop_action_UT.cpp"], + deps = [ + ":process_force_stop_action", + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/process_group_manager:mock_iprocess", + "@googletest//:gtest_main", + ], +) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/empty_force_stop_action.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/empty_force_stop_action.cpp new file mode 100644 index 0000000000..ecb4d31b82 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/empty_force_stop_action.cpp @@ -0,0 +1,24 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/force_stop_action/empty_force_stop_action.hpp" + +namespace score::mw::lifecycle::internal +{ + +Result EmptyForceStopAction::forceStop(cpp::stop_token stop_token, const Handle&) const +{ + return IComponent::RequestState::kSuccess; +} + +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/empty_force_stop_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/empty_force_stop_action.hpp new file mode 100644 index 0000000000..723c18e40c --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/empty_force_stop_action.hpp @@ -0,0 +1,35 @@ +/******************************************************************************** + * 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 SCORE_LCM_EMPTY_FORCE_STOP_ACTION_HPP_INCLUDED +#define SCORE_LCM_EMPTY_FORCE_STOP_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/force_stop_action/iforce_stop_action.hpp" + +namespace score::mw::lifecycle::internal +{ + +/// @brief A force stop action which does nothing. +class EmptyForceStopAction final : public IForceStopAction +{ + public: + /// @brief Does nothing and returns success. + /// @param stop_token Token which can be used to interrupt the action. + /// @param handle The resource to act upon. + /// @return Always successful. + Result forceStop(cpp::stop_token stop_token, const Handle&) const override; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_EMPTY_FORCE_STOP_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/empty_force_stop_action_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/empty_force_stop_action_UT.cpp new file mode 100644 index 0000000000..eb452789ab --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/empty_force_stop_action_UT.cpp @@ -0,0 +1,33 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/force_stop_action/empty_force_stop_action.hpp" +#include + +namespace score::mw::lifecycle::internal +{ +namespace +{ + +TEST(EmptyForceStopActionTest, ForceStopSucceeds) +{ + EmptyForceStopAction empty_force_stop_action; + + const Result result = empty_force_stop_action.forceStop(cpp::stop_token{}, EmptyHandle{}); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), IComponent::RequestState::kSuccess); +} + +} // namespace +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/iforce_stop_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/iforce_stop_action.hpp new file mode 100644 index 0000000000..06d0397eee --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/iforce_stop_action.hpp @@ -0,0 +1,40 @@ +/******************************************************************************** + * 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 SCORE_LCM_IFORCE_STOP_ACTION_HPP_INCLUDED +#define SCORE_LCM_IFORCE_STOP_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/handle.hpp" +#include "score/mw/launch_manager/process_group_manager/details/icomponent.hpp" +#include "score/result/result.h" +#include + +namespace score::mw::lifecycle::internal +{ + +/// @brief An action which describes how to forcefully stop a resource. +class IForceStopAction +{ + public: + virtual ~IForceStopAction() = default; + + /// @brief Forcefully stop the resource represented by the given handle. + /// @param stop_token Token which can be used to interrupt the action. + /// @param handle The resource to act upon. + /// @return Whether the action was successful, or failed with an error. + virtual Result forceStop(cpp::stop_token stop_token, const Handle& handle) const = 0; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_IFORCE_STOP_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/mock_force_stop_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/mock_force_stop_action.hpp new file mode 100644 index 0000000000..744583543e --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/mock_force_stop_action.hpp @@ -0,0 +1,35 @@ +/******************************************************************************** + * 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 MOCK_FORCE_STOP_ACTION_HPP_INCLUDED +#define MOCK_FORCE_STOP_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/force_stop_action/iforce_stop_action.hpp" +#include + +namespace score::mw::lifecycle::internal +{ + +class MockForceStopAction : public IForceStopAction +{ + public: + MOCK_METHOD( + Result, + forceStop, + (cpp::stop_token, const Handle& handle), + (override, const)); +}; + +} // namespace score::mw::lifecycle::internal + +#endif // MOCK_FORCE_STOP_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/process_force_stop_action.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/process_force_stop_action.cpp new file mode 100644 index 0000000000..a554899e7d --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/process_force_stop_action.cpp @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/force_stop_action/process_force_stop_action.hpp" +#include "score/mw/launch_manager/osal/return_types.hpp" +#include "score/mw/lifecycle/execution_error.h" + +namespace score::mw::lifecycle::internal +{ + +ProcessForceStopAction::ProcessForceStopAction(osal::IProcess& launcher) : launcher_(launcher) +{ +} + +Result ProcessForceStopAction::forceStop(cpp::stop_token stop_token, const Handle& handle) + const +{ + const ProcessHandle* const process = std::get_if(&handle); + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + process != nullptr, "ProcessForceStopAction should only be called with a ProcessHandle"); + + if (process->pid == -1) + { + return IComponent::RequestState::kSuccess; + } + + const osal::OsalReturnType result = launcher_.forceTermination(process->pid); + + if (result == osal::OsalReturnType::kSuccess) + { + return IComponent::RequestState::kWaiting; + } + + return MakeUnexpected(ExecErrc::kGeneralError); +} + +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/process_force_stop_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/process_force_stop_action.hpp new file mode 100644 index 0000000000..d7a19f365e --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/process_force_stop_action.hpp @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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 SCORE_LCM_PROCESS_FORCE_STOP_ACTION_HPP_INCLUDED +#define SCORE_LCM_PROCESS_FORCE_STOP_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/force_stop_action/iforce_stop_action.hpp" +#include "score/mw/launch_manager/process_group_manager/iprocess.hpp" +#include "score/result/result.h" + +namespace score::mw::lifecycle::internal +{ + +/// @brief A force stop action which forcefully stops a POSIX process. +/// @details The process is stopped by sending SIGKILL, which cannot be caught +/// by the process and results in immediate termination. +class ProcessForceStopAction final : public IForceStopAction +{ + public: + /// @brief Creates a new process force stop action. + /// @param launcher The process launcher used to stop the process. + explicit ProcessForceStopAction(osal::IProcess& launcher); + + /// @brief Forcefully stop the process represented by the given handle. + /// @param stop_token Token which can be used to interrupt the action. + /// @param handle The resource to act upon. + /// @return Whether the action was successful, or failed with an error. + Result forceStop(cpp::stop_token stop_token, const Handle& handle) const override; + + private: + /// @brief The process launcher used to stop the process. + osal::IProcess& launcher_; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_PROCESS_FORCE_STOP_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/process_force_stop_action_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/process_force_stop_action_UT.cpp new file mode 100644 index 0000000000..3de0ed5343 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/force_stop_action/process_force_stop_action_UT.cpp @@ -0,0 +1,99 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/force_stop_action/process_force_stop_action.hpp" +#include "score/mw/launch_manager/process_group_manager/mock_iprocess.hpp" +#include "score/mw/lifecycle/execution_error.h" +#include +#include + +namespace score::mw::lifecycle::internal +{ +namespace +{ + +using namespace ::testing; + +const ProcessHandle mock_handle = ProcessHandle{42}; + +TEST(ProcessForceStopActionTest, ForceStopCallsForceTerminationWithPid) +{ + StrictMock launcher; + const ProcessForceStopAction process_force_stop_action(launcher); + + EXPECT_CALL(launcher, forceTermination(mock_handle.pid)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + + static_cast(process_force_stop_action.forceStop(cpp::stop_token{}, mock_handle)); +} + +TEST(ProcessForceStopActionTest, ForceStopSucceedsReturnsWaiting) +{ + NiceMock launcher; + const ProcessForceStopAction process_force_stop_action(launcher); + + ON_CALL(launcher, forceTermination(_)).WillByDefault(Return(osal::OsalReturnType::kSuccess)); + + const Result result = process_force_stop_action.forceStop(cpp::stop_token{}, mock_handle); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), IComponent::RequestState::kWaiting); +} + +TEST(ProcessForceStopActionTest, ForceStopWithInvalidatedHandleReturnsSuccess) +{ + NiceMock launcher; + const ProcessForceStopAction process_force_stop_action(launcher); + ProcessHandle handle = mock_handle; + handle.pid = -1; + + const Result result = process_force_stop_action.forceStop(cpp::stop_token{}, handle); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), IComponent::RequestState::kSuccess); +} + +TEST(ProcessStopActionTest, ForceStopWithInvalidatedHandleDoesNotCallLauncher) +{ + StrictMock launcher; + const ProcessForceStopAction process_force_stop_action(launcher); + ProcessHandle handle = mock_handle; + handle.pid = -1; + + static_cast(process_force_stop_action.forceStop(cpp::stop_token{}, handle)); +} + +TEST(ProcessForceStopActionTest, ForceStopFailsReturnsError) +{ + NiceMock launcher; + const ProcessForceStopAction process_force_stop_action(launcher); + + ON_CALL(launcher, forceTermination(_)).WillByDefault(Return(osal::OsalReturnType::kFail)); + + const Result result = process_force_stop_action.forceStop(cpp::stop_token{}, mock_handle); + + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kGeneralError); +} + +TEST(ProcessForceStopActionTest, ForceStopWithEmptyHandleAborts) +{ + NiceMock launcher; + const ProcessForceStopAction process_force_stop_action(launcher); + + EXPECT_DEATH( + static_cast(process_force_stop_action.forceStop(cpp::stop_token{}, EmptyHandle{})), + "ProcessForceStopAction should only be called with a ProcessHandle"); +} + +} // namespace +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/handle.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/handle.cpp new file mode 100644 index 0000000000..6428c86213 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/handle.cpp @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/handle.hpp" + +namespace score::mw::lifecycle::internal +{ + +void invalidate(Handle& handle) +{ + if (ProcessHandle* const process_handle = std::get_if(&handle)) + { + process_handle->pid = -1; + } +} + +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/handle.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/handle.hpp new file mode 100644 index 0000000000..a6a1643cd1 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/handle.hpp @@ -0,0 +1,48 @@ +/******************************************************************************** + * 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 SCORE_LCM_HANDLE_HPP_INCLUDED +#define SCORE_LCM_HANDLE_HPP_INCLUDED + +#include +#include + +#include "score/mw/launch_manager/osal/ipc_comms.hpp" + +namespace score::mw::lifecycle::internal +{ + +/// @brief Signifies that we are not managing any resource. +/// @details This is useful for run targets and synchronisation points, +/// which exist only to depend on other components. +struct EmptyHandle +{ +}; + +/// @brief Represents a POSIX process that we are managing. +struct ProcessHandle +{ + pid_t pid; + osal::IpcCommsP sync; +}; + +/// @brief Represents some resource that we are managing. +using Handle = std::variant; + +/// @brief Changes a handle to indicate that the resource it pointed to is gone. +/// @param handle The handle to invalidate. +void invalidate(Handle& handle); + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_HANDLE_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/BUILD b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/BUILD new file mode 100644 index 0000000000..58398d843b --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/BUILD @@ -0,0 +1,94 @@ +# ******************************************************************************* +# 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("//tests/utils/bazel:unit_test.bzl", "lm_cc_test") + +cc_library( + name = "iready_condition", + hdrs = ["iready_condition.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + "//score/launch_manager/src/daemon/src/process_group_manager/details:handle", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/result", + ], +) + +cc_library( + name = "mock_ready_condition", + testonly = True, + hdrs = ["mock_ready_condition.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + ":iready_condition", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "file_ready_condition", + srcs = ["file_ready_condition.cpp"], + hdrs = ["file_ready_condition.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details/ready_condition", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + ":iready_condition", + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/configuration:component_config", + "//score/launch_manager/src/daemon/src/osal:ifile_waiter", + "//score/launch_manager/src/daemon/src/osal:return_types", + "@score_baselibs//score/language/safecpp/string_view:zstring_view", + ], +) + +lm_cc_test( + name = "file_ready_condition_UT", + srcs = ["file_ready_condition_UT.cpp"], + deps = [ + ":file_ready_condition", + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/osal:mock_ifile_waiter", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "report_running_ready_condition", + srcs = ["report_running_ready_condition.cpp"], + hdrs = ["report_running_ready_condition.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details/ready_condition", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + ":iready_condition", + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/osal:return_types", + "//score/launch_manager/src/daemon/src/process_group_manager:iprocess", + ], +) + +lm_cc_test( + name = "report_running_ready_condition_UT", + srcs = ["report_running_ready_condition_UT.cpp"], + deps = [ + ":report_running_ready_condition", + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/process_group_manager:mock_iprocess", + "@googletest//:gtest_main", + ], +) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/file_ready_condition.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/file_ready_condition.cpp new file mode 100644 index 0000000000..744433bb39 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/file_ready_condition.cpp @@ -0,0 +1,43 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/ready_condition/file_ready_condition.hpp" +#include "score/mw/launch_manager/osal/return_types.hpp" +#include "score/mw/lifecycle/execution_error.h" + +namespace score::mw::lifecycle::internal +{ + +FileReadyCondition::FileReadyCondition( + const osal::IFileWaiter& file_waiter, + score::safecpp::zstring_view file_path, + configuration::FileExistenceState state, + std::chrono::milliseconds timeout, + std::chrono::milliseconds interval) + : file_waiter_(file_waiter), file_path_(file_path), state_(state), timeout_(timeout), interval_(interval) +{ +} + +Result FileReadyCondition::wait(cpp::stop_token stop_token, [[maybe_unused]] const Handle&) const +{ + const osal::OsalReturnType result = file_waiter_.waitForFile(file_path_, state_, timeout_, interval_, stop_token); + + if (result == osal::OsalReturnType::kSuccess) + { + return {}; + } + + return MakeUnexpected(ExecErrc::kGeneralError); +} + +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/file_ready_condition.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/file_ready_condition.hpp new file mode 100644 index 0000000000..89c8061623 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/file_ready_condition.hpp @@ -0,0 +1,71 @@ +/******************************************************************************** + * 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 SCORE_LCM_FILE_READY_CONDITION_HPP_INCLUDED +#define SCORE_LCM_FILE_READY_CONDITION_HPP_INCLUDED + +#include "score/language/safecpp/string_view/zstring_view.h" +#include "score/mw/launch_manager/osal/ifile_waiter.hpp" +#include "score/mw/launch_manager/process_group_manager/details/handle.hpp" +#include "score/mw/launch_manager/process_group_manager/details/ready_condition/iready_condition.hpp" +#include "score/result/result.h" +#include + +namespace score::mw::lifecycle::internal +{ + +/// @brief A ready condition which waits for a file to exist/not exist. +/// @details This may be useful for components which mount filesystems, +/// or set up devices under `/dev`. +class FileReadyCondition final : public IReadyCondition +{ + public: + /// @brief Creates a new file ready condition. + /// @param file_waiter The waiter used to check the state of the file. + /// @param file_path The path to the file to wait for. + /// @param state The desired state of the file. + /// @param timeout The maximum duration to wait for the file condition. + /// @param interval How often to poll for the file. + explicit FileReadyCondition( + const osal::IFileWaiter& file_waiter, + score::safecpp::zstring_view file_path, + configuration::FileExistenceState state, + std::chrono::milliseconds timeout, + std::chrono::milliseconds interval); + + /// @brief Wait until the resource represented by the given handle is ready. + /// @param stop_token Token which can be used to interrupt the wait. + /// @param handle The resource to wait on. + /// @return Whether the wait was successful, or failed with an error. + Result wait(cpp::stop_token stop_token, const Handle& handle) const override; + + private: + /// @brief The waiter used to check file state. + const osal::IFileWaiter& file_waiter_; + + /// @brief The path to the file to wait for. + const score::safecpp::zstring_view file_path_; + + /// @brief The desired state of the file. + const configuration::FileExistenceState state_; + + /// @brief The maximum duration to wait for the file to exist/not exist. + const std::chrono::milliseconds timeout_; + + /// @brief How often to poll for the file. + const std::chrono::milliseconds interval_; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_FILE_READY_CONDITION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/file_ready_condition_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/file_ready_condition_UT.cpp new file mode 100644 index 0000000000..a944efac58 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/file_ready_condition_UT.cpp @@ -0,0 +1,100 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/osal/mock_ifile_waiter.hpp" +#include "score/mw/launch_manager/process_group_manager/details/ready_condition/file_ready_condition.hpp" +#include "score/mw/lifecycle/execution_error.h" +#include +#include + +namespace score::mw::lifecycle::internal +{ +namespace +{ + +using namespace ::testing; +using namespace std::chrono_literals; + +constexpr score::safecpp::zstring_view kFilePath = "/some/path"; + +TEST(FileReadyConditionTest, WaitCallsWaitForFile) +{ + StrictMock file_waiter; + const FileReadyCondition file_ready_condition( + file_waiter, kFilePath, configuration::FileExistenceState::Exists, 10ms, 5ms); + + EXPECT_CALL( + file_waiter, waitForFile(Eq(kFilePath), Eq(configuration::FileExistenceState::Exists), Eq(10ms), Eq(5ms), _)) + .WillOnce(Return(osal::OsalReturnType::kSuccess)); + + static_cast(file_ready_condition.wait(cpp::stop_token{}, EmptyHandle{})); +} + +TEST(FileReadyConditionTest, WaitSucceedsReturnsSuccess) +{ + NiceMock file_waiter; + const FileReadyCondition file_ready_condition( + file_waiter, kFilePath, configuration::FileExistenceState::Exists, 10ms, 5ms); + + ON_CALL(file_waiter, waitForFile(_, _, _, _, _)).WillByDefault(Return(osal::OsalReturnType::kSuccess)); + + const Result result = file_ready_condition.wait(cpp::stop_token{}, EmptyHandle{}); + + EXPECT_TRUE(result.has_value()); +} + +TEST(FileReadyConditionTest, WaitFailsReturnsError) +{ + NiceMock file_waiter; + const FileReadyCondition file_ready_condition( + file_waiter, kFilePath, configuration::FileExistenceState::Exists, 10ms, 5ms); + + ON_CALL(file_waiter, waitForFile(_, _, _, _, _)).WillByDefault(Return(osal::OsalReturnType::kFail)); + + const Result result = file_ready_condition.wait(cpp::stop_token{}, EmptyHandle{}); + + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kGeneralError); +} + +TEST(FileReadyConditionTest, WaitTimesOutReturnsError) +{ + NiceMock file_waiter; + const FileReadyCondition file_ready_condition( + file_waiter, kFilePath, configuration::FileExistenceState::Exists, 10ms, 5ms); + + ON_CALL(file_waiter, waitForFile(_, _, _, _, _)).WillByDefault(Return(osal::OsalReturnType::kTimeout)); + + const Result result = file_ready_condition.wait(cpp::stop_token{}, EmptyHandle{}); + + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kGeneralError); +} + +TEST(FileReadyConditionTest, WaitAcceptsVariousHandles) +{ + NiceMock file_waiter; + const FileReadyCondition file_ready_condition( + file_waiter, kFilePath, configuration::FileExistenceState::Exists, 10ms, 5ms); + + ON_CALL(file_waiter, waitForFile(_, _, _, _, _)).WillByDefault(Return(osal::OsalReturnType::kSuccess)); + + const Result result_empty = file_ready_condition.wait(cpp::stop_token{}, EmptyHandle{}); + EXPECT_TRUE(result_empty.has_value()); + + const Result result_process = file_ready_condition.wait(cpp::stop_token{}, ProcessHandle{}); + EXPECT_TRUE(result_process.has_value()); +} + +} // namespace +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/iready_condition.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/iready_condition.hpp new file mode 100644 index 0000000000..590befebfd --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/iready_condition.hpp @@ -0,0 +1,50 @@ +/******************************************************************************** + * 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 SCORE_LCM_IREADY_CONDITION_HPP_INCLUDED +#define SCORE_LCM_IREADY_CONDITION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/handle.hpp" +#include "score/result/result.h" +#include + +namespace score::mw::lifecycle::internal +{ + +/// @brief A condition which decides when a resource has finished its startup. +class IReadyCondition +{ + public: + virtual ~IReadyCondition() = default; + + /// @brief Wait until the resource represented by the given handle is ready. + /// @param stop_token Token which can be used to interrupt the wait. + /// @param handle The resource to wait on. + /// @return Whether the wait was successful, or failed with an error. + virtual Result wait(cpp::stop_token stop_token, const Handle& handle) const = 0; + + /// @brief Notify the ready condition that a POSIX process has terminated. + /// @param handle The process that terminated. + /// @param status Exit code of the process. + /// @return Whether this ready condition accepts the event. + // FIXME: This should be a more generic event which makes sense for any + // handle type. + virtual bool tryHandleTermination(const Handle& handle, int32_t status) const + { + return false; + } +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_IREADY_CONDITION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/mock_ready_condition.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/mock_ready_condition.hpp new file mode 100644 index 0000000000..231516d4ad --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/mock_ready_condition.hpp @@ -0,0 +1,31 @@ +/******************************************************************************** + * 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 MOCK_READY_CONDITION_HPP_INCLUDED +#define MOCK_READY_CONDITION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/ready_condition/iready_condition.hpp" +#include + +namespace score::mw::lifecycle::internal +{ + +class MockReadyCondition : public IReadyCondition +{ + public: + MOCK_METHOD(Result, wait, (cpp::stop_token, const Handle& handle), (override, const)); +}; + +} // namespace score::mw::lifecycle::internal + +#endif // MOCK_READY_CONDITION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/report_running_ready_condition.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/report_running_ready_condition.cpp new file mode 100644 index 0000000000..fe3eb18afd --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/report_running_ready_condition.cpp @@ -0,0 +1,66 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/ready_condition/report_running_ready_condition.hpp" +#include "score/mw/launch_manager/osal/return_types.hpp" +#include "score/mw/lifecycle/execution_error.h" + +namespace score::mw::lifecycle::internal +{ + +ReportRunningReadyCondition::ReportRunningReadyCondition( + osal::IProcess& launcher, + std::optional timeout) + : launcher_(launcher), timeout_(timeout) +{ +} + +Result ReportRunningReadyCondition::wait(cpp::stop_token stop_token, const Handle& handle) const +{ + const ProcessHandle* const process = std::get_if(&handle); + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + process != nullptr, "ReportRunningReadyCondition should only be called with a ProcessHandle"); + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + process->sync != nullptr, + "ReportRunningReadyCondition should only be called with a ProcessHandle containing IPCCommsSync"); + + const osal::OsalReturnType result = launcher_.waitForkRunning(process->sync, timeout_); + + // If the PID is invalid, the process died and the notification was sent + // from tryHandleTermination rather than the process itself. + if (result == osal::OsalReturnType::kSuccess && process->pid > 0) + { + return {}; + } + + return MakeUnexpected(ExecErrc::kGeneralError); +} + +bool ReportRunningReadyCondition::tryHandleTermination(const Handle& handle, int32_t status) const +{ + const ProcessHandle* const process = std::get_if(&handle); + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + process != nullptr, "ReportRunningReadyCondition should only be called with a ProcessHandle"); + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + process->sync != nullptr, + "ReportRunningReadyCondition should only be called with a ProcessHandle containing IPCCommsSync"); + + // FIXME: This static_cast was copied from the old code at + // https://github.com/eclipse-score/lifecycle/blob/876c5df184ee0008017334303c12f1c2ee75e42e/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp#L171 + // Should errors be handled here? + static_cast(process->sync->send_sync_.post()); + + return true; +} + +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/report_running_ready_condition.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/report_running_ready_condition.hpp new file mode 100644 index 0000000000..996ac1ec12 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/report_running_ready_condition.hpp @@ -0,0 +1,61 @@ +/******************************************************************************** + * 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 SCORE_LCM_REPORT_RUNNING_HPP_INCLUDED +#define SCORE_LCM_REPORT_RUNNING_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/handle.hpp" +#include "score/mw/launch_manager/process_group_manager/details/ready_condition/iready_condition.hpp" +#include "score/mw/launch_manager/process_group_manager/iprocess.hpp" +#include "score/result/result.h" +#include +#include + +namespace score::mw::lifecycle::internal +{ + +/// @brief A ready condition which waits for a process to report running +/// through a shared memory channel. +class ReportRunningReadyCondition final : public IReadyCondition +{ + public: + /// @brief Creates a new report running ready condition. + /// @param launcher The process launcher used to wait for the process. + /// @param timeout The maximum duration to wait for the process to report running, + /// or no timeout if not specified. + explicit ReportRunningReadyCondition(osal::IProcess& launcher, std::optional timeout); + + /// @brief Wait until the resource represented by the given handle is ready. + /// @param stop_token Token which can be used to interrupt the wait. + /// @param handle The resource to wait on. + /// @return Whether the wait was successful, or failed with an error. + Result wait(cpp::stop_token stop_token, const Handle& handle) const override; + + /// @brief Notify the ready condition that a POSIX process has terminated. + /// @param handle The process that terminated. + /// @param status Exit code of the process. + /// @return Whether this ready condition accepts the event. + bool tryHandleTermination(const Handle& handle, int32_t status) const override; + + private: + /// @brief The process launcher used to wait for the process. + osal::IProcess& launcher_; + + /// @brief The maximum duration to wait for the process to report running, + /// or no timeout if not specified. + const std::optional timeout_; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_REPORT_RUNNING_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/report_running_ready_condition_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/report_running_ready_condition_UT.cpp new file mode 100644 index 0000000000..5f72fd7a3a --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/ready_condition/report_running_ready_condition_UT.cpp @@ -0,0 +1,113 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/ready_condition/report_running_ready_condition.hpp" +#include "score/mw/launch_manager/process_group_manager/mock_iprocess.hpp" +#include "score/mw/lifecycle/execution_error.h" +#include +#include + +namespace score::mw::lifecycle::internal +{ +namespace +{ + +using namespace ::testing; +using namespace std::chrono_literals; + +alignas(osal::IpcCommsSync) std::array sync_buffer; + +// The semaphores are never touched, since the launcher is mocked. +const ProcessHandle mock_handle = ProcessHandle{ + 42, + osal::IpcCommsP{reinterpret_cast(sync_buffer.data()), [](osal::IpcCommsSync*) { + }}}; + +TEST(ReportRunningReadyConditionTest, WaitCallsWaitForkRunningWithSyncAndTimeout) +{ + StrictMock launcher; + const ReportRunningReadyCondition report_running_ready_condition(launcher, 10ms); + + EXPECT_CALL(launcher, waitForkRunning(mock_handle.sync, std::optional(10ms))) + .WillOnce(Return(osal::OsalReturnType::kSuccess)); + + static_cast(report_running_ready_condition.wait(cpp::stop_token{}, mock_handle)); +} + +TEST(ReportRunningReadyConditionTest, WaitSucceedsReturnsSuccess) +{ + NiceMock launcher; + const ReportRunningReadyCondition report_running_ready_condition(launcher, std::nullopt); + + ON_CALL(launcher, waitForkRunning(_, _)).WillByDefault(Return(osal::OsalReturnType::kSuccess)); + + const Result result = report_running_ready_condition.wait(cpp::stop_token{}, mock_handle); + + EXPECT_TRUE(result.has_value()); +} + +TEST(ReportRunningReadyConditionTest, WaitFailsReturnsError) +{ + NiceMock launcher; + const ReportRunningReadyCondition report_running_ready_condition(launcher, std::nullopt); + + ON_CALL(launcher, waitForkRunning(_, _)).WillByDefault(Return(osal::OsalReturnType::kFail)); + + const Result result = report_running_ready_condition.wait(cpp::stop_token{}, mock_handle); + + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kGeneralError); +} + +TEST(ReportRunningReadyConditionTest, WaitWithEmptyHandleAborts) +{ + NiceMock launcher; + const ReportRunningReadyCondition report_running_ready_condition(launcher, std::nullopt); + + EXPECT_DEATH( + static_cast(report_running_ready_condition.wait(cpp::stop_token{}, EmptyHandle{})), + "ReportRunningReadyCondition should only be called with a ProcessHandle"); +} + +TEST(ReportRunningReadyConditionTest, WaitWithNullSyncAborts) +{ + NiceMock launcher; + const ReportRunningReadyCondition report_running_ready_condition(launcher, std::nullopt); + + EXPECT_DEATH( + static_cast(report_running_ready_condition.wait(cpp::stop_token{}, ProcessHandle{42, nullptr})), + "ReportRunningReadyCondition should only be called with a ProcessHandle containing IPCCommsSync"); +} + +TEST(ReportRunningReadyConditionTest, TryHandleTerminationWithEmptyHandleAborts) +{ + NiceMock launcher; + const ReportRunningReadyCondition report_running_ready_condition(launcher, std::nullopt); + + EXPECT_DEATH( + static_cast(report_running_ready_condition.tryHandleTermination(EmptyHandle{}, 0)), + "ReportRunningReadyCondition should only be called with a ProcessHandle"); +} + +TEST(ReportRunningReadyConditionTest, TryHandleTerminationWithNullSyncAborts) +{ + NiceMock launcher; + const ReportRunningReadyCondition report_running_ready_condition(launcher, std::nullopt); + + EXPECT_DEATH( + static_cast(report_running_ready_condition.tryHandleTermination(ProcessHandle{42, nullptr}, 0)), + "ReportRunningReadyCondition should only be called with a ProcessHandle containing IPCCommsSync"); +} + +} // namespace +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/BUILD b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/BUILD new file mode 100644 index 0000000000..b8cb1aad9e --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/BUILD @@ -0,0 +1,86 @@ +# ******************************************************************************* +# 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("//tests/utils/bazel:unit_test.bzl", "lm_cc_test") + +cc_library( + name = "istart_action", + hdrs = ["istart_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + "//score/launch_manager/src/daemon/src/process_group_manager/details:handle", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/result", + ], +) + +cc_library( + name = "mock_start_action", + testonly = True, + hdrs = ["mock_start_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + ":istart_action", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "empty_start_action", + srcs = ["empty_start_action.cpp"], + hdrs = ["empty_start_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details/start_action", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details/start_action", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [":istart_action"], +) + +lm_cc_test( + name = "empty_start_action_UT", + srcs = ["empty_start_action_UT.cpp"], + deps = [ + ":empty_start_action", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "process_start_action", + srcs = ["process_start_action.cpp"], + hdrs = ["process_start_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details/start_action", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details/start_action", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + ":istart_action", + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/configuration:component_config", + "//score/launch_manager/src/daemon/src/osal:return_types", + "//score/launch_manager/src/daemon/src/process_group_manager:iprocess", + ], +) + +lm_cc_test( + name = "process_start_action_UT", + srcs = ["process_start_action_UT.cpp"], + deps = [ + ":process_start_action", + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/process_group_manager:mock_iprocess", + "@googletest//:gtest_main", + ], +) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/empty_start_action.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/empty_start_action.cpp new file mode 100644 index 0000000000..89df46bc84 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/empty_start_action.cpp @@ -0,0 +1,24 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/start_action/empty_start_action.hpp" + +namespace score::mw::lifecycle::internal +{ + +Result EmptyStartAction::start(cpp::stop_token stop_token) const +{ + return EmptyHandle{}; +} + +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/empty_start_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/empty_start_action.hpp new file mode 100644 index 0000000000..f9b9641d9d --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/empty_start_action.hpp @@ -0,0 +1,34 @@ +/******************************************************************************** + * 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 SCORE_LCM_EMPTY_START_ACTION_HPP_INCLUDED +#define SCORE_LCM_EMPTY_START_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/start_action/istart_action.hpp" + +namespace score::mw::lifecycle::internal +{ + +/// @brief A start action which does nothing. +class EmptyStartAction final : public IStartAction +{ + public: + /// @brief Does nothing and returns an empty handle. + /// @param stop_token Token which can be used to interrupt the action. + /// @return An empty handle. + Result start(cpp::stop_token stop_token) const override; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_EMPTY_START_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/empty_start_action_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/empty_start_action_UT.cpp new file mode 100644 index 0000000000..321dd13bc6 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/empty_start_action_UT.cpp @@ -0,0 +1,41 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/start_action/empty_start_action.hpp" +#include + +namespace score::mw::lifecycle::internal +{ +namespace +{ + +TEST(EmptyStartActionTest, StartSucceeds) +{ + EmptyStartAction empty_start_action; + + const auto result = empty_start_action.start(cpp::stop_token{}); + + EXPECT_TRUE(result.has_value()); +} + +TEST(EmptyStartActionTest, StartReturnsEmptyHandle) +{ + EmptyStartAction empty_start_action; + + const auto result = empty_start_action.start(cpp::stop_token{}); + + EXPECT_TRUE(std::holds_alternative(result.value())); +} + +} // namespace +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/istart_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/istart_action.hpp new file mode 100644 index 0000000000..3b50e65e8e --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/istart_action.hpp @@ -0,0 +1,38 @@ +/******************************************************************************** + * 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 SCORE_LCM_ISTART_ACTION_HPP_INCLUDED +#define SCORE_LCM_ISTART_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/handle.hpp" +#include "score/result/result.h" +#include + +namespace score::mw::lifecycle::internal +{ + +/// @brief An action which describes how to start a resource. +class IStartAction +{ + public: + virtual ~IStartAction() = default; + + /// @brief Start the resource. + /// @param stop_token Token which can be used to interrupt the action. + /// @return The handle of the started resource, or an error if the action failed. + virtual Result start(cpp::stop_token stop_token) const = 0; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_ISTART_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/mock_start_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/mock_start_action.hpp new file mode 100644 index 0000000000..7f8bbb4d18 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/mock_start_action.hpp @@ -0,0 +1,31 @@ +/******************************************************************************** + * 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 MOCK_START_ACTION_HPP_INCLUDED +#define MOCK_START_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/start_action/istart_action.hpp" +#include + +namespace score::mw::lifecycle::internal +{ + +class MockStartAction : public IStartAction +{ + public: + MOCK_METHOD(Result, start, (cpp::stop_token), (override, const)); +}; + +} // namespace score::mw::lifecycle::internal + +#endif // MOCK_START_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/process_start_action.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/process_start_action.cpp new file mode 100644 index 0000000000..d306aaa5d8 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/process_start_action.cpp @@ -0,0 +1,41 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/start_action/process_start_action.hpp" +#include "score/mw/launch_manager/osal/return_types.hpp" +#include "score/mw/lifecycle/execution_error.h" + +namespace score::mw::lifecycle::internal +{ + +ProcessStartAction::ProcessStartAction(osal::IProcess& launcher, const configuration::ComponentConfig& config) + : launcher_(launcher), config_(config) +{ +} + +Result ProcessStartAction::start(cpp::stop_token stop_token) const +{ + osal::ProcessID pid = 0; + osal::IpcCommsP sync = nullptr; + + const auto result = launcher_.startProcess(pid, sync, config_); + + if (result == osal::OsalReturnType::kSuccess) + { + return ProcessHandle{pid, sync}; + } + + return MakeUnexpected(ExecErrc::kGeneralError); +} + +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/process_start_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/process_start_action.hpp new file mode 100644 index 0000000000..8adbdc27bc --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/process_start_action.hpp @@ -0,0 +1,51 @@ +/******************************************************************************** + * 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 SCORE_LCM_PROCESS_START_ACTION_HPP_INCLUDED +#define SCORE_LCM_PROCESS_START_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/configuration/component_config.hpp" +#include "score/mw/launch_manager/process_group_manager/details/handle.hpp" +#include "score/mw/launch_manager/process_group_manager/details/start_action/istart_action.hpp" +#include "score/mw/launch_manager/process_group_manager/iprocess.hpp" +#include "score/result/result.h" + +namespace score::mw::lifecycle::internal +{ + +/// @brief A start action which starts a POSIX process. +class ProcessStartAction final : public IStartAction +{ + public: + /// @brief Creates a new process start action. + /// @param launcher The process launcher used to start the process. + /// @param config Configuration describing the executable to launch + /// and its environment. + ProcessStartAction(osal::IProcess& launcher, const configuration::ComponentConfig& config); + + /// @brief Start a process. + /// @param stop_token Token which can be used to interrupt the action. + /// @return The handle of the started process, or an error if the action failed. + Result start(cpp::stop_token stop_token) const override; + + private: + /// @brief The process launcher used to start the process. + osal::IProcess& launcher_; + + /// @brief Configuration describing the executable to launch and its environment. + const configuration::ComponentConfig& config_; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_PROCESS_START_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/process_start_action_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/process_start_action_UT.cpp new file mode 100644 index 0000000000..1de9c8f3aa --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/start_action/process_start_action_UT.cpp @@ -0,0 +1,69 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/start_action/process_start_action.hpp" +#include "score/mw/launch_manager/process_group_manager/mock_iprocess.hpp" +#include "score/mw/lifecycle/execution_error.h" +#include +#include + +namespace score::mw::lifecycle::internal +{ +namespace +{ + +using namespace ::testing; + +TEST(ProcessStartActionTest, StartCallsStartProcessWithConfig) +{ + StrictMock launcher; + const configuration::ComponentConfig config{}; + const ProcessStartAction process_start_action(launcher, config); + + EXPECT_CALL(launcher, startProcess(_, _, Ref(config))).WillOnce(Return(osal::OsalReturnType::kSuccess)); + + static_cast(process_start_action.start(cpp::stop_token{})); +} + +TEST(ProcessStartActionTest, StartSucceedsReturnsProcessHandle) +{ + NiceMock launcher; + const configuration::ComponentConfig config{}; + const ProcessStartAction process_start_action(launcher, config); + const osal::ProcessID expected_pid = 42; + + ON_CALL(launcher, startProcess(_, _, _)) + .WillByDefault(DoAll(SetArgReferee<0>(expected_pid), Return(osal::OsalReturnType::kSuccess))); + + const Result result = process_start_action.start(cpp::stop_token{}); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(std::get(result.value()).pid, expected_pid); +} + +TEST(ProcessStartActionTest, StartFailsReturnsError) +{ + NiceMock launcher; + const configuration::ComponentConfig config{}; + const ProcessStartAction process_start_action(launcher, config); + + ON_CALL(launcher, startProcess(_, _, _)).WillByDefault(Return(osal::OsalReturnType::kFail)); + + const Result result = process_start_action.start(cpp::stop_token{}); + + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kGeneralError); +} + +} // namespace +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/BUILD b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/BUILD new file mode 100644 index 0000000000..4e2911c814 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/BUILD @@ -0,0 +1,86 @@ +# ******************************************************************************* +# 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("//tests/utils/bazel:unit_test.bzl", "lm_cc_test") + +cc_library( + name = "istop_action", + hdrs = ["istop_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + "//score/launch_manager/src/daemon/src/process_group_manager/details:handle", + "//score/launch_manager/src/daemon/src/process_group_manager/details:icomponent", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/result", + ], +) + +cc_library( + name = "mock_stop_action", + testonly = True, + hdrs = ["mock_stop_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + ":istop_action", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "empty_stop_action", + srcs = ["empty_stop_action.cpp"], + hdrs = ["empty_stop_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details/stop_action", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [":istop_action"], +) + +lm_cc_test( + name = "empty_stop_action_UT", + srcs = ["empty_stop_action_UT.cpp"], + deps = [ + ":empty_stop_action", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "process_stop_action", + srcs = ["process_stop_action.cpp"], + hdrs = ["process_stop_action.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details/stop_action", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__subpackages__"], + deps = [ + ":istop_action", + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/osal:return_types", + "//score/launch_manager/src/daemon/src/process_group_manager:iprocess", + ], +) + +lm_cc_test( + name = "process_stop_action_UT", + srcs = ["process_stop_action_UT.cpp"], + deps = [ + ":process_stop_action", + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/process_group_manager:mock_iprocess", + "@googletest//:gtest_main", + ], +) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/empty_stop_action.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/empty_stop_action.cpp new file mode 100644 index 0000000000..14cd03a53d --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/empty_stop_action.cpp @@ -0,0 +1,24 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/stop_action/empty_stop_action.hpp" + +namespace score::mw::lifecycle::internal +{ + +Result EmptyStopAction::stop(cpp::stop_token stop_token, const Handle&) const +{ + return IComponent::RequestState::kSuccess; +} + +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/empty_stop_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/empty_stop_action.hpp new file mode 100644 index 0000000000..38f3f1309d --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/empty_stop_action.hpp @@ -0,0 +1,35 @@ +/******************************************************************************** + * 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 SCORE_LCM_EMPTY_STOP_ACTION_HPP_INCLUDED +#define SCORE_LCM_EMPTY_STOP_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/stop_action/istop_action.hpp" + +namespace score::mw::lifecycle::internal +{ + +/// @brief A stop action which does nothing. +class EmptyStopAction final : public IStopAction +{ + public: + /// @brief Does nothing and returns success. + /// @param stop_token Token which can be used to interrupt the action. + /// @param handle The resource to act upon. + /// @return Always successful. + Result stop(cpp::stop_token stop_token, const Handle&) const override; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_EMPTY_STOP_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/empty_stop_action_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/empty_stop_action_UT.cpp new file mode 100644 index 0000000000..120007ff57 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/empty_stop_action_UT.cpp @@ -0,0 +1,33 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/stop_action/empty_stop_action.hpp" +#include + +namespace score::mw::lifecycle::internal +{ +namespace +{ + +TEST(EmptyStopActionTest, StopSucceeds) +{ + EmptyStopAction empty_stop_action; + + const Result result = empty_stop_action.stop(cpp::stop_token{}, EmptyHandle{}); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), IComponent::RequestState::kSuccess); +} + +} // namespace +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/istop_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/istop_action.hpp new file mode 100644 index 0000000000..1e9e91bc73 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/istop_action.hpp @@ -0,0 +1,40 @@ +/******************************************************************************** + * 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 SCORE_LCM_ISTOP_ACTION_HPP_INCLUDED +#define SCORE_LCM_ISTOP_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/handle.hpp" +#include "score/mw/launch_manager/process_group_manager/details/icomponent.hpp" +#include "score/result/result.h" +#include + +namespace score::mw::lifecycle::internal +{ + +/// @brief An action which describes how to gracefully stop a resource. +class IStopAction +{ + public: + virtual ~IStopAction() = default; + + /// @brief Gracefully stop the resource represented by the given handle. + /// @param stop_token Token which can be used to interrupt the action. + /// @param handle The resource to act upon. + /// @return Whether the action was successful, or failed with an error. + virtual Result stop(cpp::stop_token stop_token, const Handle& handle) const = 0; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_ISTOP_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/mock_stop_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/mock_stop_action.hpp new file mode 100644 index 0000000000..9a3488ac24 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/mock_stop_action.hpp @@ -0,0 +1,31 @@ +/******************************************************************************** + * 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 MOCK_STOP_ACTION_HPP_INCLUDED +#define MOCK_STOP_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/stop_action/istop_action.hpp" +#include + +namespace score::mw::lifecycle::internal +{ + +class MockStopAction : public IStopAction +{ + public: + MOCK_METHOD(Result, stop, (cpp::stop_token, const Handle& handle), (override, const)); +}; + +} // namespace score::mw::lifecycle::internal + +#endif // MOCK_STOP_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/process_stop_action.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/process_stop_action.cpp new file mode 100644 index 0000000000..680fd41250 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/process_stop_action.cpp @@ -0,0 +1,46 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/stop_action/process_stop_action.hpp" +#include "score/mw/launch_manager/osal/return_types.hpp" +#include "score/mw/lifecycle/execution_error.h" + +namespace score::mw::lifecycle::internal +{ + +ProcessStopAction::ProcessStopAction(osal::IProcess& launcher) : launcher_(launcher) +{ +} + +Result ProcessStopAction::stop(cpp::stop_token stop_token, const Handle& handle) const +{ + const ProcessHandle* const process = std::get_if(&handle); + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + process != nullptr, "ProcessStopAction should only be called with a ProcessHandle"); + + if (process->pid == -1) + { + return IComponent::RequestState::kSuccess; + } + + const osal::OsalReturnType result = launcher_.requestTermination(process->pid); + + if (result == osal::OsalReturnType::kSuccess) + { + return IComponent::RequestState::kWaiting; + } + + return MakeUnexpected(ExecErrc::kGeneralError); +} + +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/process_stop_action.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/process_stop_action.hpp new file mode 100644 index 0000000000..4e76ad72c1 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/process_stop_action.hpp @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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 SCORE_LCM_PROCESS_STOP_ACTION_HPP_INCLUDED +#define SCORE_LCM_PROCESS_STOP_ACTION_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/details/stop_action/istop_action.hpp" +#include "score/mw/launch_manager/process_group_manager/iprocess.hpp" +#include "score/result/result.h" + +namespace score::mw::lifecycle::internal +{ + +/// @brief A stop action which gracefully stops a POSIX process. +/// @details The process is stopped by sending SIGTERM, which the process may handle +/// to perform cleanup before exiting. +class ProcessStopAction final : public IStopAction +{ + public: + /// @brief Creates a new process stop action. + /// @param launcher The process launcher used to stop the process. + explicit ProcessStopAction(osal::IProcess& launcher); + + /// @brief Gracefully stop the process represented by the given handle. + /// @param stop_token Token which can be used to interrupt the action. + /// @param handle The resource to act upon. + /// @return Whether the action was successful, or failed with an error. + Result stop(cpp::stop_token stop_token, const Handle& handle) const override; + + private: + /// @brief The process launcher used to stop the process. + osal::IProcess& launcher_; +}; + +} // namespace score::mw::lifecycle::internal + +#endif // SCORE_LCM_PROCESS_STOP_ACTION_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/process_stop_action_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/process_stop_action_UT.cpp new file mode 100644 index 0000000000..2a4bd66215 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/stop_action/process_stop_action_UT.cpp @@ -0,0 +1,99 @@ +/******************************************************************************** + * 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 "score/mw/launch_manager/process_group_manager/details/stop_action/process_stop_action.hpp" +#include "score/mw/launch_manager/process_group_manager/mock_iprocess.hpp" +#include "score/mw/lifecycle/execution_error.h" +#include +#include + +namespace score::mw::lifecycle::internal +{ +namespace +{ + +using namespace ::testing; + +const ProcessHandle mock_handle = ProcessHandle{42}; + +TEST(ProcessStopActionTest, StopCallsRequestTerminationWithPid) +{ + StrictMock launcher; + const ProcessStopAction process_stop_action(launcher); + + EXPECT_CALL(launcher, requestTermination(mock_handle.pid)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + + static_cast(process_stop_action.stop(cpp::stop_token{}, mock_handle)); +} + +TEST(ProcessStopActionTest, StopSucceedsReturnsWaiting) +{ + NiceMock launcher; + const ProcessStopAction process_stop_action(launcher); + + ON_CALL(launcher, requestTermination(_)).WillByDefault(Return(osal::OsalReturnType::kSuccess)); + + const Result result = process_stop_action.stop(cpp::stop_token{}, mock_handle); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), IComponent::RequestState::kWaiting); +} + +TEST(ProcessStopActionTest, StopWithInvalidatedHandleReturnsSuccess) +{ + NiceMock launcher; + const ProcessStopAction process_stop_action(launcher); + ProcessHandle handle = mock_handle; + handle.pid = -1; + + const Result result = process_stop_action.stop(cpp::stop_token{}, handle); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), IComponent::RequestState::kSuccess); +} + +TEST(ProcessStopActionTest, StopWithInvalidatedHandleDoesNotCallLauncher) +{ + StrictMock launcher; + const ProcessStopAction process_stop_action(launcher); + ProcessHandle handle = mock_handle; + handle.pid = -1; + + static_cast(process_stop_action.stop(cpp::stop_token{}, handle)); +} + +TEST(ProcessStopActionTest, StopFailsReturnsError) +{ + NiceMock launcher; + const ProcessStopAction process_stop_action(launcher); + + ON_CALL(launcher, requestTermination(_)).WillByDefault(Return(osal::OsalReturnType::kFail)); + + const Result result = process_stop_action.stop(cpp::stop_token{}, mock_handle); + + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kGeneralError); +} + +TEST(ProcessStopActionTest, StopWithEmptyHandleAborts) +{ + NiceMock launcher; + const ProcessStopAction process_stop_action(launcher); + + EXPECT_DEATH( + static_cast(process_stop_action.stop(cpp::stop_token{}, EmptyHandle{})), + "ProcessStopAction should only be called with a ProcessHandle"); +} + +} // namespace +} // namespace score::mw::lifecycle::internal