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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions tests/integration/shutdown_signal/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
load("//tests/utils/bazel:integration.bzl", "integration_test")

cc_library(
name = "shutdown_signal_common",
hdrs = ["common.hpp"],
)

cc_binary(
name = "control_daemon_mock",
srcs = ["control_daemon_mock.cpp"],
deps = [
":shutdown_signal_common",
"//score/launch_manager:control_cc",
"//score/launch_manager:lifecycle_cc",
"//tests/utils/test_helper",
"@googletest//:gtest_main",
],
)

cc_binary(
name = "shutdown_signal_process",
srcs = ["shutdown_signal_process.cpp"],
deps = [
":shutdown_signal_common",
"//score/launch_manager:control_cc",
"//score/launch_manager:lifecycle_cc",
"//tests/utils/test_helper",
"@googletest//:gtest_main",
],
)

integration_test(
name = "shutdown_signal",
srcs = ["shutdown_signal.py"],
binaries = [
":control_daemon_mock",
":shutdown_signal_process",
"//score/launch_manager",
],
config = ":shutdown_signal.json",
)
25 changes: 25 additions & 0 deletions tests/integration/shutdown_signal/common.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/********************************************************************************
* 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_TESTS_INTEGRATION_SHUTDOWN_SIGNAL_COMMON_HPP
#define SCORE_TESTS_INTEGRATION_SHUTDOWN_SIGNAL_COMMON_HPP

#include <string_view>

/// @brief Written by gtest_process from within its SIGTERM handler, containing
/// its PID as raw bytes. Its existence proves the Launch Manager delivered a
/// SIGTERM to request shutdown, and the PID lets control_daemon_mock confirm the
/// process was subsequently killed. It is written before the process blocks, so
/// it survives the subsequent SIGKILL (no code can run after SIGKILL).
constexpr std::string_view sigterm_received_file = "sigterm_received";

#endif // SCORE_TESTS_INTEGRATION_SHUTDOWN_SIGNAL_COMMON_HPP
91 changes: 91 additions & 0 deletions tests/integration/shutdown_signal/control_daemon_mock.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/********************************************************************************
* 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 <gtest/gtest.h>
#include <cerrno>
#include <csignal>
#include <filesystem>
Comment thread
MaciejKaszynski marked this conversation as resolved.
#include <fstream>

#include "common.hpp"
#include "tests/utils/test_helper/test_helper.hpp"
#include <score/mw/lifecycle/control_client.h>
#include <score/mw/lifecycle/report_running.h>

// The Launch Manager shall shut a process down by sending it a SIGTERM, and, if
// the process does not terminate itself in time, a SIGKILL.
//
// The managed shutdown_signal_process installs a SIGTERM handler that records the SIGTERM
// (writing its PID to `sigterm_received_file`) and then blocks instead of
// terminating, forcing the Launch Manager to send SIGKILL.
//
// We drive the shutdown by activating "Running" (which starts shutdown_signal_process) and
// then switching back to "Startup". "Startup" no longer depends on shutdown_signal_process,
// so it is terminated, while the control daemon itself stays alive (it is part of
// "Startup") and can therefore assert the outcome. Switching to "Off" instead
// would terminate the control daemon too, so it could not run the assertion.
TEST(ShutdownSignal, Daemon)
{
score::mw::lifecycle::ControlClient client{};
ASSERT_TRUE(check_clean({test_end_location, sigterm_received_file}));

TEST_STEP("Control daemon report running")
{
score::mw::lifecycle::report_running();
}

TEST_STEP("Activate RunTarget Running")
{
score::cpp::stop_token stop_token;
auto result = client.ActivateRunTarget("Running").Get(stop_token);
EXPECT_TRUE(result.has_value()) << "Activating target Running failed: " << result.error().Message();
}

// Switching away from "Running" terminates shutdown_signal_process. Because it does not
// self-terminate on SIGTERM, the Launch Manager must escalate to SIGKILL for
// the transition to complete.
TEST_STEP("Activate RunTarget Startup")
{
score::cpp::stop_token stop_token;
auto result = client.ActivateRunTarget("Startup").Get(stop_token);
EXPECT_TRUE(result.has_value()) << "Activating target Startup failed: " << result.error().Message();
}

TEST_STEP("Verify SIGTERM was received and SIGKILL forced termination")
{
// SIGTERM was delivered: the process recorded its PID before blocking.
ASSERT_TRUE(std::filesystem::exists(sigterm_received_file))
<< "shutdown_signal_process did not receive a SIGTERM during shutdown";

// Read back the PID the process wrote from within its SIGTERM handler.
pid_t pid{};
std::ifstream pid_file{std::string{sigterm_received_file}, std::ios::binary};
ASSERT_TRUE(pid_file.read(reinterpret_cast<char*>(&pid), sizeof(pid)))
<< "Failed to read the PID from " << sigterm_received_file;

// SIGKILL forced termination: the process never self-terminates, so its
// absence proves the Launch Manager escalated to SIGKILL.
EXPECT_EQ(kill(pid, 0), -1) << "shutdown_signal_process (pid " << pid
<< ") is still alive; it was not SIGKILLed";
EXPECT_EQ(errno, ESRCH) << "unexpected errno probing shutdown_signal_process (pid " << pid << ")";
}

TEST_STEP("Activate RunTarget Off")
{
client.ActivateRunTarget("Off");
}
}

int main()
{
return TestRunner(__FILE__, TerminationBehavior::kWait, TerminationNotification::kTestEnd).RunTests();
}
90 changes: 90 additions & 0 deletions tests/integration/shutdown_signal/shutdown_signal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"schema_version": 1,
"defaults": {
"deployment_config": {
Comment thread
MaciejKaszynski marked this conversation as resolved.
"bin_dir": "/tmp/tests/shutdown_signal",
"ready_timeout": 1.0,
"shutdown_timeout": 1.0,
"ready_recovery_action": {
"restart": {
"number_of_attempts": 0
}
},
"sandbox": {
"uid": 0,
"gid": 0,
"scheduling_policy": "SCHED_OTHER",
"scheduling_priority": 0
}
}
},
"components": {
"control_daemon": {
"component_properties": {
"binary_name": "control_daemon_mock",
"application_profile": {
"application_type": "State_Manager",
"alive_supervision": {
"min_indications": 0
}
}
},
"deployment_config": {
"ready_timeout": 1.0,
"shutdown_timeout": 1.0,
"environmental_variables": {
"PROCESSIDENTIFIER": "control_daemon"
}
}
},
"shutdown_signal_process": {
"component_properties": {
"binary_name": "shutdown_signal_process",
"application_profile": {
"application_type": "Reporting"
}
},
"deployment_config": {
"shutdown_timeout": 0.5,
"environmental_variables": {
"PROCESSIDENTIFIER": "DefaultPG_app0"
}
}
}
},
"run_targets": {
"Startup": {
"depends_on": [
"control_daemon"
],
"recovery_action": {
"switch_run_target": {
"run_target": "fallback_run_target"
}
}
},
"Running": {
"depends_on": [
"control_daemon",
"shutdown_signal_process"
],
"recovery_action": {
"switch_run_target": {
"run_target": "fallback_run_target"
}
}
},
"Off": {
"depends_on": [],
"recovery_action": {
"switch_run_target": {
"run_target": "fallback_run_target"
}
}
}
},
"initial_run_target": "Startup",
"fallback_run_target": {
"depends_on": []
}
}
56 changes: 56 additions & 0 deletions tests/integration/shutdown_signal/shutdown_signal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
from tests.utils.testing_utils.run_until_file_deployed import run_until_file_deployed
from tests.utils.testing_utils.setup_test import setup_test
from tests.utils.testing_utils.test_results import assert_test_results
from attribute_plugin import add_test_properties


@add_test_properties(
fully_verifies=[
"comp_req__launch_man__shutdown_signal",
],
partially_verifies=[],
test_type="requirements-based",
derivation_technique="requirements-analysis",
)
def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_dir):
"""
Objective: Verifies that the Launch Manager shuts a process down by sending a
SIGTERM and, if the process does not terminate itself in time, escalates to a
SIGKILL.

The control daemon activates the "Running" run target (starting the managed
shutdown_signal_process), then switches back to "Startup". The shutdown_signal_process installs a
SIGTERM handler that records its PID and then deliberately blocks instead of
terminating, forcing the Launch Manager to send SIGKILL. Finally the control
daemon activates "Off".

Expected Behaviour: shutdown_signal_process receives a SIGTERM (proven by the
`sigterm_received` file, which holds the PID it wrote before blocking and
therefore survives SIGKILL) and is then force-terminated by SIGKILL (proven by
that PID no longer existing, since the process never self-terminates).
"""

new_config_path = str(remote_test_dir / "etc/shutdown_signal.bin")

run_until_file_deployed(
target=target,
binary_path=str(remote_test_dir / "launch_manager"),
file_path=remote_test_dir.parent / "test_end",
cwd=str(remote_test_dir),
args=["-c", new_config_path],
timeout_s=10.0,
)

assert_test_results({"control_daemon_mock.xml", "shutdown_signal_process.xml"})
71 changes: 71 additions & 0 deletions tests/integration/shutdown_signal/shutdown_signal_process.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/********************************************************************************
Comment thread
MaciejKaszynski marked this conversation as resolved.
* 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 "common.hpp"
#include "tests/utils/test_helper/test_helper.hpp"
#include <fcntl.h>
#include <gtest/gtest.h>
#include <score/mw/lifecycle/report_running.h>
#include <unistd.h>
#include <csignal>

namespace
{
/// @brief SIGTERM handler installed by the process under test.
///
/// It records its PID (so the outcome can be verified even after the process is
/// gone, since no code runs after SIGKILL) and then blocks forever instead of
/// terminating. Because it never self-terminates, the Launch Manager must
/// escalate to SIGKILL to shut it down; the recorded PID then lets
/// control_daemon_mock confirm that the process is truly gone.
void shutdownSignalHandler(int /*signum*/)
{
// getpid()/open()/write()/pause() are all async-signal-safe, so this is safe
// to run from within a signal handler. The PID is written as raw bytes; no
// string encoding is needed. On a write failure nothing is recorded, which
// fails the SIGTERM assertion rather than masquerading as a graceful exit.
const pid_t pid = getpid();
const int fd = open(sigterm_received_file.data(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0)
{
static_cast<void>(write(fd, &pid, sizeof(pid)));
static_cast<void>(close(fd));
}

// Do NOT terminate: block until SIGKILL arrives so shutdown requires it.
while (true)
{
static_cast<void>(pause());
}
}
} // namespace

TEST(ShutdownSignal, Process)
{
// Remove any leftover file from a previous manual run.
ASSERT_TRUE(check_clean({sigterm_received_file}, false));

// Install our own SIGTERM handler. This must happen after the TestRunner
// constructor (which registers its default handler), so that ours takes
// precedence for the shutdown signal sent by the Launch Manager.
signal(SIGTERM, shutdownSignalHandler);

// Report running so the Launch Manager considers this process ready and the
// "Running" run target can be activated.
score::mw::lifecycle::report_running();
}

int main()
{
return TestRunner(__FILE__).RunTests();
}
Loading