From 9c3613994d861d8ac6d0c0a2b3fc0dfcef676a29 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 27 Jul 2026 17:25:24 +0200 Subject: [PATCH 01/10] Add test for comp_req__lifecycle__shutdown_signal --- tests/integration/shutdown_signal/BUILD | 55 +++++++++ tests/integration/shutdown_signal/common.hpp | 31 +++++ .../shutdown_signal/control_daemon_mock.cpp | 80 ++++++++++++ .../shutdown_signal/gtest_process.cpp | 79 ++++++++++++ .../shutdown_signal/shutdown_signal.json | 116 ++++++++++++++++++ .../shutdown_signal/shutdown_signal.py | 68 ++++++++++ 6 files changed, 429 insertions(+) create mode 100644 tests/integration/shutdown_signal/BUILD create mode 100644 tests/integration/shutdown_signal/common.hpp create mode 100644 tests/integration/shutdown_signal/control_daemon_mock.cpp create mode 100644 tests/integration/shutdown_signal/gtest_process.cpp create mode 100644 tests/integration/shutdown_signal/shutdown_signal.json create mode 100644 tests/integration/shutdown_signal/shutdown_signal.py diff --git a/tests/integration/shutdown_signal/BUILD b/tests/integration/shutdown_signal/BUILD new file mode 100644 index 0000000000..6adfb6a64e --- /dev/null +++ b/tests/integration/shutdown_signal/BUILD @@ -0,0 +1,55 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +load("@rules_cc//cc:cc_library.bzl", "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 = "gtest_process", + srcs = ["gtest_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", + ":gtest_process", + "//score/launch_manager", + ], + config = ":shutdown_signal.json", +) diff --git a/tests/integration/shutdown_signal/common.hpp b/tests/integration/shutdown_signal/common.hpp new file mode 100644 index 0000000000..8420e630cf --- /dev/null +++ b/tests/integration/shutdown_signal/common.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 SCORE_TESTS_INTEGRATION_SHUTDOWN_SIGNAL_COMMON_HPP +#define SCORE_TESTS_INTEGRATION_SHUTDOWN_SIGNAL_COMMON_HPP + +#include + +/// @brief Written by gtest_process from within its SIGTERM handler. Its +/// existence proves the Launch Manager delivered a SIGTERM to request shutdown. +/// It is written before the process starts sleeping, so it survives the +/// subsequent SIGKILL (no code can run after SIGKILL). +constexpr std::string_view sigterm_received_file = "sigterm_received"; + +/// @brief Written by gtest_process only if its SIGTERM handler ever returns from +/// the (long) sleep, i.e. if it was allowed to shut down gracefully. Because the +/// process intentionally sleeps past its shutdown_timeout, the Launch Manager +/// must escalate to SIGKILL, which interrupts the sleep. Hence this file must +/// NOT exist: its absence proves SIGKILL forcibly terminated the process. +constexpr std::string_view graceful_exit_file = "graceful_exit"; + +#endif // SCORE_TESTS_INTEGRATION_SHUTDOWN_SIGNAL_COMMON_HPP diff --git a/tests/integration/shutdown_signal/control_daemon_mock.cpp b/tests/integration/shutdown_signal/control_daemon_mock.cpp new file mode 100644 index 0000000000..a35a380a3f --- /dev/null +++ b/tests/integration/shutdown_signal/control_daemon_mock.cpp @@ -0,0 +1,80 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include +#include + +#include "common.hpp" +#include "tests/utils/test_helper/test_helper.hpp" +#include +#include + +// 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 gtest_process installs a SIGTERM handler that records the SIGTERM +// (writing `sigterm_received_file`) and then sleeps past its shutdown_timeout +// instead of terminating, forcing the Launch Manager to send SIGKILL. +// +// We drive the shutdown by activating "Running" (which starts gtest_process) and +// then switching back to "Startup". "Startup" no longer depends on gtest_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, graceful_exit_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 gtest_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 it before sleeping. + EXPECT_TRUE(std::filesystem::exists(sigterm_received_file)) + << "gtest_process did not receive a SIGTERM during shutdown"; + // SIGKILL forced termination: the process was killed mid-sleep and thus + // never reached the point where it would have flagged a graceful exit. + EXPECT_FALSE(std::filesystem::exists(graceful_exit_file)) + << "gtest_process was not force-terminated with SIGKILL; it exited its sleep gracefully"; + } + + TEST_STEP("Activate RunTarget Off") + { + client.ActivateRunTarget("Off"); + } +} + +int main() +{ + return TestRunner(__FILE__, TerminationBehavior::kWait, TerminationNotification::kTestEnd).RunTests(); +} diff --git a/tests/integration/shutdown_signal/gtest_process.cpp b/tests/integration/shutdown_signal/gtest_process.cpp new file mode 100644 index 0000000000..df150a38d4 --- /dev/null +++ b/tests/integration/shutdown_signal/gtest_process.cpp @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include +#include +#include +#include + +#include "common.hpp" +#include "tests/utils/test_helper/test_helper.hpp" +#include + +namespace +{ +/// @brief How long the process sleeps after receiving SIGTERM. This must be +/// clearly larger than the configured shutdown_timeout so that the process does +/// not terminate itself in time, forcing the Launch Manager to send SIGKILL. +constexpr unsigned int kSleepAfterSigtermSeconds = 5U; + +/// @brief Creates an empty file using only async-signal-safe calls so it is safe +/// to invoke from within a signal handler. +void createFileAsyncSignalSafe(const std::string_view path) +{ + const int fd = open(path.data(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) + { + static_cast(close(fd)); + } +} + +/// @brief SIGTERM handler installed by the process under test. +/// +/// It records that a SIGTERM was received (so this can be verified even after +/// the process is gone, since no code runs after SIGKILL) and then deliberately +/// sleeps past the configured shutdown_timeout instead of terminating. This +/// forces the Launch Manager to escalate to SIGKILL. If the sleep ever returns +/// (i.e. SIGKILL did not arrive), a second file is written to flag the failure. +void shutdownSignalHandler(int /*signum*/) +{ + createFileAsyncSignalSafe(sigterm_received_file); + + // Do NOT terminate: outlast the shutdown_timeout so SIGKILL is required. + static_cast(sleep(kSleepAfterSigtermSeconds)); + + // Reaching this point means we were not SIGKILLed - record graceful exit so + // the assertion in control_daemon_mock can detect that SIGKILL did not work. + createFileAsyncSignalSafe(graceful_exit_file); +} +} // namespace + +TEST(ShutdownSignal, Process) +{ + // Remove any leftover files from a previous manual run. + ASSERT_TRUE(check_clean({sigterm_received_file, graceful_exit_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(); +} diff --git a/tests/integration/shutdown_signal/shutdown_signal.json b/tests/integration/shutdown_signal/shutdown_signal.json new file mode 100644 index 0000000000..dbfcdd0445 --- /dev/null +++ b/tests/integration/shutdown_signal/shutdown_signal.json @@ -0,0 +1,116 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/shutdown_signal", + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + }, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib" + }, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1 + } + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "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" + } + } + }, + "gtest_process": { + "component_properties": { + "binary_name": "gtest_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", + "gtest_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", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [] + } +} diff --git a/tests/integration/shutdown_signal/shutdown_signal.py b/tests/integration/shutdown_signal/shutdown_signal.py new file mode 100644 index 0000000000..0e2f92030f --- /dev/null +++ b/tests/integration/shutdown_signal/shutdown_signal.py @@ -0,0 +1,68 @@ +# ******************************************************************************* +# 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__lifecycle__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 + gtest_process), then switches back to "Startup". The gtest_process installs a + SIGTERM handler that records the received SIGTERM and then deliberately sleeps + past its shutdown_timeout instead of terminating, forcing the Launch Manager + to send SIGKILL. Finally the control daemon activates "Off". + + Expected Behaviour: gtest_process receives a SIGTERM (proven by the + `sigterm_received` file, which is written before the sleep and therefore + survives SIGKILL) and is then force-terminated by SIGKILL (proven by the + absence of the `graceful_exit` file, which would only exist had the process + been allowed to finish sleeping). Both assertions are checked in + control_daemon_mock after the "Startup" transition succeeds. + """ + + # launch manager will simply ignore the arguments if run with --//config:use_new_configuration=False. + # the old configuration will be used, which is the default behavior. + # The new configuration will be used if run with --//config:use_new_configuration=True + new_config_path = str(remote_test_dir / "etc/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, + ) + + # Additionally verify on the target filesystem that the SIGTERM was received + # and that the process was force-killed (no graceful exit was recorded). + res, _ = target.execute(f"test -f {remote_test_dir / 'sigterm_received'}") + assert res == 0, "gtest_process did not receive a SIGTERM during shutdown" + res, _ = target.execute(f"test -f {remote_test_dir / 'graceful_exit'}") + assert res != 0, "gtest_process was not force-terminated with SIGKILL" + + assert_test_results({"control_daemon_mock.xml", "gtest_process.xml"}) From bcee8608adfaec6d391cdf7e7f80664bccdec2e7 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 5 Aug 2026 13:54:50 +0200 Subject: [PATCH 02/10] Rename comp to feat --- tests/integration/shutdown_signal/shutdown_signal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/shutdown_signal/shutdown_signal.py b/tests/integration/shutdown_signal/shutdown_signal.py index 0e2f92030f..bc63c45ba1 100644 --- a/tests/integration/shutdown_signal/shutdown_signal.py +++ b/tests/integration/shutdown_signal/shutdown_signal.py @@ -18,7 +18,7 @@ @add_test_properties( fully_verifies=[ - "comp_req__lifecycle__shutdown_signal", + "feat_req__lifecycle__shutdown_signal", ], partially_verifies=[], test_type="requirements-based", From a2857ac3fbc8cd65232e339c959cd8410f53e161 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 5 Aug 2026 17:08:29 +0200 Subject: [PATCH 03/10] Rename process --- tests/integration/shutdown_signal/BUILD | 9 ++++----- .../shutdown_signal/control_daemon_mock.cpp | 12 ++++++------ .../integration/shutdown_signal/shutdown_signal.json | 6 +++--- tests/integration/shutdown_signal/shutdown_signal.py | 10 +++++----- ...gtest_process.cpp => shutdown_signal_process.cpp} | 0 5 files changed, 18 insertions(+), 19 deletions(-) rename tests/integration/shutdown_signal/{gtest_process.cpp => shutdown_signal_process.cpp} (100%) diff --git a/tests/integration/shutdown_signal/BUILD b/tests/integration/shutdown_signal/BUILD index 6adfb6a64e..bc47bd49de 100644 --- a/tests/integration/shutdown_signal/BUILD +++ b/tests/integration/shutdown_signal/BUILD @@ -10,8 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_cc//cc:cc_binary.bzl", "cc_binary") -load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") load("//tests/utils/bazel:integration.bzl", "integration_test") cc_library( @@ -32,8 +31,8 @@ cc_binary( ) cc_binary( - name = "gtest_process", - srcs = ["gtest_process.cpp"], + name = "shutdown_signal_process", + srcs = ["shutdown_signal_process.cpp"], deps = [ ":shutdown_signal_common", "//score/launch_manager:control_cc", @@ -48,7 +47,7 @@ integration_test( srcs = ["shutdown_signal.py"], binaries = [ ":control_daemon_mock", - ":gtest_process", + ":shutdown_signal_process", "//score/launch_manager", ], config = ":shutdown_signal.json", diff --git a/tests/integration/shutdown_signal/control_daemon_mock.cpp b/tests/integration/shutdown_signal/control_daemon_mock.cpp index a35a380a3f..c8ea8bc015 100644 --- a/tests/integration/shutdown_signal/control_daemon_mock.cpp +++ b/tests/integration/shutdown_signal/control_daemon_mock.cpp @@ -21,12 +21,12 @@ // 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 gtest_process installs a SIGTERM handler that records the SIGTERM +// The managed shutdown_signal_process installs a SIGTERM handler that records the SIGTERM // (writing `sigterm_received_file`) and then sleeps past its shutdown_timeout // instead of terminating, forcing the Launch Manager to send SIGKILL. // -// We drive the shutdown by activating "Running" (which starts gtest_process) and -// then switching back to "Startup". "Startup" no longer depends on gtest_process, +// 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. @@ -47,7 +47,7 @@ TEST(ShutdownSignal, Daemon) EXPECT_TRUE(result.has_value()) << "Activating target Running failed: " << result.error().Message(); } - // Switching away from "Running" terminates gtest_process. Because it does not + // 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") @@ -61,11 +61,11 @@ TEST(ShutdownSignal, Daemon) { // SIGTERM was delivered: the process recorded it before sleeping. EXPECT_TRUE(std::filesystem::exists(sigterm_received_file)) - << "gtest_process did not receive a SIGTERM during shutdown"; + << "shutdown_signal_process did not receive a SIGTERM during shutdown"; // SIGKILL forced termination: the process was killed mid-sleep and thus // never reached the point where it would have flagged a graceful exit. EXPECT_FALSE(std::filesystem::exists(graceful_exit_file)) - << "gtest_process was not force-terminated with SIGKILL; it exited its sleep gracefully"; + << "shutdown_signal_process was not force-terminated with SIGKILL; it exited its sleep gracefully"; } TEST_STEP("Activate RunTarget Off") diff --git a/tests/integration/shutdown_signal/shutdown_signal.json b/tests/integration/shutdown_signal/shutdown_signal.json index dbfcdd0445..4f8b127f96 100644 --- a/tests/integration/shutdown_signal/shutdown_signal.json +++ b/tests/integration/shutdown_signal/shutdown_signal.json @@ -60,9 +60,9 @@ } } }, - "gtest_process": { + "shutdown_signal_process": { "component_properties": { - "binary_name": "gtest_process", + "binary_name": "shutdown_signal_process", "application_profile": { "application_type": "Reporting" } @@ -89,7 +89,7 @@ "Running": { "depends_on": [ "control_daemon", - "gtest_process" + "shutdown_signal_process" ], "recovery_action": { "switch_run_target": { diff --git a/tests/integration/shutdown_signal/shutdown_signal.py b/tests/integration/shutdown_signal/shutdown_signal.py index bc63c45ba1..b776a642fc 100644 --- a/tests/integration/shutdown_signal/shutdown_signal.py +++ b/tests/integration/shutdown_signal/shutdown_signal.py @@ -31,12 +31,12 @@ def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_di SIGKILL. The control daemon activates the "Running" run target (starting the managed - gtest_process), then switches back to "Startup". The gtest_process installs a + shutdown_signal_process), then switches back to "Startup". The shutdown_signal_process installs a SIGTERM handler that records the received SIGTERM and then deliberately sleeps past its shutdown_timeout instead of terminating, forcing the Launch Manager to send SIGKILL. Finally the control daemon activates "Off". - Expected Behaviour: gtest_process receives a SIGTERM (proven by the + Expected Behaviour: shutdown_signal_process receives a SIGTERM (proven by the `sigterm_received` file, which is written before the sleep and therefore survives SIGKILL) and is then force-terminated by SIGKILL (proven by the absence of the `graceful_exit` file, which would only exist had the process @@ -61,8 +61,8 @@ def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_di # Additionally verify on the target filesystem that the SIGTERM was received # and that the process was force-killed (no graceful exit was recorded). res, _ = target.execute(f"test -f {remote_test_dir / 'sigterm_received'}") - assert res == 0, "gtest_process did not receive a SIGTERM during shutdown" + assert res == 0, "shutdown_signal_process did not receive a SIGTERM during shutdown" res, _ = target.execute(f"test -f {remote_test_dir / 'graceful_exit'}") - assert res != 0, "gtest_process was not force-terminated with SIGKILL" + assert res != 0, "shutdown_signal_process was not force-terminated with SIGKILL" - assert_test_results({"control_daemon_mock.xml", "gtest_process.xml"}) + assert_test_results({"control_daemon_mock.xml", "shutdown_signal_process.xml"}) diff --git a/tests/integration/shutdown_signal/gtest_process.cpp b/tests/integration/shutdown_signal/shutdown_signal_process.cpp similarity index 100% rename from tests/integration/shutdown_signal/gtest_process.cpp rename to tests/integration/shutdown_signal/shutdown_signal_process.cpp From c0540010f711e84749f06da2c7be09628dfd6f70 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 5 Aug 2026 17:23:53 +0200 Subject: [PATCH 04/10] Strip down cfg and comments --- .../shutdown_signal/shutdown_signal.json | 26 ------------------- .../shutdown_signal/shutdown_signal.py | 3 --- 2 files changed, 29 deletions(-) diff --git a/tests/integration/shutdown_signal/shutdown_signal.json b/tests/integration/shutdown_signal/shutdown_signal.json index 4f8b127f96..a1bfc06223 100644 --- a/tests/integration/shutdown_signal/shutdown_signal.json +++ b/tests/integration/shutdown_signal/shutdown_signal.json @@ -10,35 +10,12 @@ "number_of_attempts": 0 } }, - "recovery_action": { - "switch_run_target": { - "run_target": "fallback_run_target" - } - }, - "environmental_variables": { - "LD_LIBRARY_PATH": "/opt/lib" - }, "sandbox": { "uid": 0, "gid": 0, "scheduling_policy": "SCHED_OTHER", "scheduling_priority": 0 } - }, - "component_properties": { - "application_profile": { - "application_type": "Reporting", - "is_self_terminating": false, - "alive_supervision": { - "reporting_cycle": 0.1, - "min_indications": 1, - "max_indications": 3, - "failed_cycles_tolerance": 1 - } - }, - "ready_condition": { - "process_state": "Running" - } } }, "components": { @@ -107,9 +84,6 @@ } }, "initial_run_target": "Startup", - "alive_supervision": { - "evaluation_cycle": 0.05 - }, "fallback_run_target": { "depends_on": [] } diff --git a/tests/integration/shutdown_signal/shutdown_signal.py b/tests/integration/shutdown_signal/shutdown_signal.py index b776a642fc..b8d527bf74 100644 --- a/tests/integration/shutdown_signal/shutdown_signal.py +++ b/tests/integration/shutdown_signal/shutdown_signal.py @@ -44,9 +44,6 @@ def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_di control_daemon_mock after the "Startup" transition succeeds. """ - # launch manager will simply ignore the arguments if run with --//config:use_new_configuration=False. - # the old configuration will be used, which is the default behavior. - # The new configuration will be used if run with --//config:use_new_configuration=True new_config_path = str(remote_test_dir / "etc/shutdown_signal.bin") run_until_file_deployed( From 7b250a16e319bf0222458cc4e59d7b2c77093e37 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 6 Aug 2026 09:08:23 +0200 Subject: [PATCH 05/10] Remove duplicated assertion from test --- tests/integration/shutdown_signal/shutdown_signal.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/integration/shutdown_signal/shutdown_signal.py b/tests/integration/shutdown_signal/shutdown_signal.py index b8d527bf74..e0510ef504 100644 --- a/tests/integration/shutdown_signal/shutdown_signal.py +++ b/tests/integration/shutdown_signal/shutdown_signal.py @@ -55,11 +55,4 @@ def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_di timeout_s=10.0, ) - # Additionally verify on the target filesystem that the SIGTERM was received - # and that the process was force-killed (no graceful exit was recorded). - res, _ = target.execute(f"test -f {remote_test_dir / 'sigterm_received'}") - assert res == 0, "shutdown_signal_process did not receive a SIGTERM during shutdown" - res, _ = target.execute(f"test -f {remote_test_dir / 'graceful_exit'}") - assert res != 0, "shutdown_signal_process was not force-terminated with SIGKILL" - assert_test_results({"control_daemon_mock.xml", "shutdown_signal_process.xml"}) From 727e75571cc734303e8fc519f035e45436c7bd58 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 6 Aug 2026 10:25:24 +0200 Subject: [PATCH 06/10] exit on file system failure and write message --- .../shutdown_signal/shutdown_signal_process.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/integration/shutdown_signal/shutdown_signal_process.cpp b/tests/integration/shutdown_signal/shutdown_signal_process.cpp index df150a38d4..e4b1ab38fd 100644 --- a/tests/integration/shutdown_signal/shutdown_signal_process.cpp +++ b/tests/integration/shutdown_signal/shutdown_signal_process.cpp @@ -15,7 +15,6 @@ #include #include #include - #include "common.hpp" #include "tests/utils/test_helper/test_helper.hpp" #include @@ -32,10 +31,18 @@ constexpr unsigned int kSleepAfterSigtermSeconds = 5U; void createFileAsyncSignalSafe(const std::string_view path) { const int fd = open(path.data(), O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd >= 0) + if (fd < 0) { - static_cast(close(fd)); + // write()/_exit() are async-signal-safe; std::cerr/std::exit are not. + static constexpr char prefix[] = "[FAILED] Failed to create file "; + static constexpr char suffix[] = " in signal handler\n"; + static_cast(write(STDERR_FILENO, prefix, sizeof(prefix) - 1)); + static_cast(write(STDERR_FILENO, path.data(), path.size())); + static_cast(write(STDERR_FILENO, suffix, sizeof(suffix) - 1)); + static_cast(unlink(path.data())); // leave no partial file + _exit(-1); } + static_cast(close(fd)); } /// @brief SIGTERM handler installed by the process under test. From 1ae444872a1a8fe0178493c4f3ee01b128509be7 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 6 Aug 2026 10:52:48 +0200 Subject: [PATCH 07/10] Verify in python that no file system error appeared --- tests/integration/shutdown_signal/shutdown_signal.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integration/shutdown_signal/shutdown_signal.py b/tests/integration/shutdown_signal/shutdown_signal.py index e0510ef504..380899dfe3 100644 --- a/tests/integration/shutdown_signal/shutdown_signal.py +++ b/tests/integration/shutdown_signal/shutdown_signal.py @@ -46,7 +46,7 @@ def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_di new_config_path = str(remote_test_dir / "etc/shutdown_signal.bin") - run_until_file_deployed( + lm_process = run_until_file_deployed( target=target, binary_path=str(remote_test_dir / "launch_manager"), file_path=remote_test_dir.parent / "test_end", @@ -55,4 +55,11 @@ def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_di timeout_s=10.0, ) + # shutdown_signal_process prints this line on a file system failure; guard + # against it explicitly so a write error can never masquerade as a SIGKILL. + assert "[FAILED] Failed to create file" not in lm_process.get_output(), ( + "shutdown_signal_process hit a filesystem error writing a marker file; " + "the graceful_exit assertion would be unreliable" + ) + assert_test_results({"control_daemon_mock.xml", "shutdown_signal_process.xml"}) From ca058e5e679e0b56e287a16d1019bfc3e1f43325 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 6 Aug 2026 13:36:20 +0200 Subject: [PATCH 08/10] feature req is now a component req --- tests/integration/shutdown_signal/shutdown_signal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/shutdown_signal/shutdown_signal.py b/tests/integration/shutdown_signal/shutdown_signal.py index 380899dfe3..9adbba1eb0 100644 --- a/tests/integration/shutdown_signal/shutdown_signal.py +++ b/tests/integration/shutdown_signal/shutdown_signal.py @@ -18,7 +18,7 @@ @add_test_properties( fully_verifies=[ - "feat_req__lifecycle__shutdown_signal", + "comp_req__launch_man__shutdown_signal", ], partially_verifies=[], test_type="requirements-based", From 251ac870f9f600b3a38c0a46d3f378fdd397b706 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 6 Aug 2026 13:44:55 +0200 Subject: [PATCH 09/10] Rename graceful_exit to sigkill_not_received --- tests/integration/shutdown_signal/common.hpp | 8 +++----- tests/integration/shutdown_signal/control_daemon_mock.cpp | 4 ++-- tests/integration/shutdown_signal/shutdown_signal.py | 4 ++-- .../shutdown_signal/shutdown_signal_process.cpp | 4 ++-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/integration/shutdown_signal/common.hpp b/tests/integration/shutdown_signal/common.hpp index 8420e630cf..5dcf998551 100644 --- a/tests/integration/shutdown_signal/common.hpp +++ b/tests/integration/shutdown_signal/common.hpp @@ -22,10 +22,8 @@ constexpr std::string_view sigterm_received_file = "sigterm_received"; /// @brief Written by gtest_process only if its SIGTERM handler ever returns from -/// the (long) sleep, i.e. if it was allowed to shut down gracefully. Because the -/// process intentionally sleeps past its shutdown_timeout, the Launch Manager -/// must escalate to SIGKILL, which interrupts the sleep. Hence this file must -/// NOT exist: its absence proves SIGKILL forcibly terminated the process. -constexpr std::string_view graceful_exit_file = "graceful_exit"; +/// the (long) sleep, i.e. if it was allowed to shut down gracefully and did not +/// receive a SIGKILL. Its absence proves the Launch Manager escalated to SIGKILL. +constexpr std::string_view sigkill_not_received_file = "sigkill_not_received"; #endif // SCORE_TESTS_INTEGRATION_SHUTDOWN_SIGNAL_COMMON_HPP diff --git a/tests/integration/shutdown_signal/control_daemon_mock.cpp b/tests/integration/shutdown_signal/control_daemon_mock.cpp index c8ea8bc015..a9d7108442 100644 --- a/tests/integration/shutdown_signal/control_daemon_mock.cpp +++ b/tests/integration/shutdown_signal/control_daemon_mock.cpp @@ -33,7 +33,7 @@ TEST(ShutdownSignal, Daemon) { score::mw::lifecycle::ControlClient client{}; - ASSERT_TRUE(check_clean({test_end_location, sigterm_received_file, graceful_exit_file})); + ASSERT_TRUE(check_clean({test_end_location, sigterm_received_file, sigkill_not_received_file})); TEST_STEP("Control daemon report running") { @@ -64,7 +64,7 @@ TEST(ShutdownSignal, Daemon) << "shutdown_signal_process did not receive a SIGTERM during shutdown"; // SIGKILL forced termination: the process was killed mid-sleep and thus // never reached the point where it would have flagged a graceful exit. - EXPECT_FALSE(std::filesystem::exists(graceful_exit_file)) + EXPECT_FALSE(std::filesystem::exists(sigkill_not_received_file)) << "shutdown_signal_process was not force-terminated with SIGKILL; it exited its sleep gracefully"; } diff --git a/tests/integration/shutdown_signal/shutdown_signal.py b/tests/integration/shutdown_signal/shutdown_signal.py index 9adbba1eb0..006cf10b22 100644 --- a/tests/integration/shutdown_signal/shutdown_signal.py +++ b/tests/integration/shutdown_signal/shutdown_signal.py @@ -39,7 +39,7 @@ def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_di Expected Behaviour: shutdown_signal_process receives a SIGTERM (proven by the `sigterm_received` file, which is written before the sleep and therefore survives SIGKILL) and is then force-terminated by SIGKILL (proven by the - absence of the `graceful_exit` file, which would only exist had the process + absence of the `sigkill_not_received` file, which would only exist had the process been allowed to finish sleeping). Both assertions are checked in control_daemon_mock after the "Startup" transition succeeds. """ @@ -59,7 +59,7 @@ def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_di # against it explicitly so a write error can never masquerade as a SIGKILL. assert "[FAILED] Failed to create file" not in lm_process.get_output(), ( "shutdown_signal_process hit a filesystem error writing a marker file; " - "the graceful_exit assertion would be unreliable" + "the sigkill_not_received assertion would be unreliable" ) assert_test_results({"control_daemon_mock.xml", "shutdown_signal_process.xml"}) diff --git a/tests/integration/shutdown_signal/shutdown_signal_process.cpp b/tests/integration/shutdown_signal/shutdown_signal_process.cpp index e4b1ab38fd..6c42f803f2 100644 --- a/tests/integration/shutdown_signal/shutdown_signal_process.cpp +++ b/tests/integration/shutdown_signal/shutdown_signal_process.cpp @@ -61,14 +61,14 @@ void shutdownSignalHandler(int /*signum*/) // Reaching this point means we were not SIGKILLed - record graceful exit so // the assertion in control_daemon_mock can detect that SIGKILL did not work. - createFileAsyncSignalSafe(graceful_exit_file); + createFileAsyncSignalSafe(sigkill_not_received_file); } } // namespace TEST(ShutdownSignal, Process) { // Remove any leftover files from a previous manual run. - ASSERT_TRUE(check_clean({sigterm_received_file, graceful_exit_file}, false)); + ASSERT_TRUE(check_clean({sigterm_received_file, sigkill_not_received_file}, false)); // Install our own SIGTERM handler. This must happen after the TestRunner // constructor (which registers its default handler), so that ours takes From 25df5344756957341d4674b3921c641438386e16 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 7 Aug 2026 12:26:50 +0200 Subject: [PATCH 10/10] No longer needed to check in python for log output PID is written to file which is created on sigterm. control_daemon_mock checks for existence of file and later on if pid is gone. --- tests/integration/shutdown_signal/common.hpp | 14 ++-- .../shutdown_signal/control_daemon_mock.cpp | 29 +++++--- .../shutdown_signal/shutdown_signal.py | 23 ++----- .../shutdown_signal_process.cpp | 69 ++++++++----------- 4 files changed, 59 insertions(+), 76 deletions(-) diff --git a/tests/integration/shutdown_signal/common.hpp b/tests/integration/shutdown_signal/common.hpp index 5dcf998551..6e233ca40f 100644 --- a/tests/integration/shutdown_signal/common.hpp +++ b/tests/integration/shutdown_signal/common.hpp @@ -15,15 +15,11 @@ #include -/// @brief Written by gtest_process from within its SIGTERM handler. Its -/// existence proves the Launch Manager delivered a SIGTERM to request shutdown. -/// It is written before the process starts sleeping, so it survives the -/// subsequent SIGKILL (no code can run after SIGKILL). +/// @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"; -/// @brief Written by gtest_process only if its SIGTERM handler ever returns from -/// the (long) sleep, i.e. if it was allowed to shut down gracefully and did not -/// receive a SIGKILL. Its absence proves the Launch Manager escalated to SIGKILL. -constexpr std::string_view sigkill_not_received_file = "sigkill_not_received"; - #endif // SCORE_TESTS_INTEGRATION_SHUTDOWN_SIGNAL_COMMON_HPP diff --git a/tests/integration/shutdown_signal/control_daemon_mock.cpp b/tests/integration/shutdown_signal/control_daemon_mock.cpp index a9d7108442..e10c6db689 100644 --- a/tests/integration/shutdown_signal/control_daemon_mock.cpp +++ b/tests/integration/shutdown_signal/control_daemon_mock.cpp @@ -11,7 +11,10 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ #include +#include +#include #include +#include #include "common.hpp" #include "tests/utils/test_helper/test_helper.hpp" @@ -22,8 +25,8 @@ // the process does not terminate itself in time, a SIGKILL. // // The managed shutdown_signal_process installs a SIGTERM handler that records the SIGTERM -// (writing `sigterm_received_file`) and then sleeps past its shutdown_timeout -// instead of terminating, forcing the Launch Manager to send SIGKILL. +// (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, @@ -33,7 +36,7 @@ TEST(ShutdownSignal, Daemon) { score::mw::lifecycle::ControlClient client{}; - ASSERT_TRUE(check_clean({test_end_location, sigterm_received_file, sigkill_not_received_file})); + ASSERT_TRUE(check_clean({test_end_location, sigterm_received_file})); TEST_STEP("Control daemon report running") { @@ -59,13 +62,21 @@ TEST(ShutdownSignal, Daemon) TEST_STEP("Verify SIGTERM was received and SIGKILL forced termination") { - // SIGTERM was delivered: the process recorded it before sleeping. - EXPECT_TRUE(std::filesystem::exists(sigterm_received_file)) + // 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"; - // SIGKILL forced termination: the process was killed mid-sleep and thus - // never reached the point where it would have flagged a graceful exit. - EXPECT_FALSE(std::filesystem::exists(sigkill_not_received_file)) - << "shutdown_signal_process was not force-terminated with SIGKILL; it exited its sleep gracefully"; + + // 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(&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") diff --git a/tests/integration/shutdown_signal/shutdown_signal.py b/tests/integration/shutdown_signal/shutdown_signal.py index 006cf10b22..3ae87407bb 100644 --- a/tests/integration/shutdown_signal/shutdown_signal.py +++ b/tests/integration/shutdown_signal/shutdown_signal.py @@ -32,21 +32,19 @@ def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_di 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 the received SIGTERM and then deliberately sleeps - past its shutdown_timeout instead of terminating, forcing the Launch Manager - to send SIGKILL. Finally the control daemon activates "Off". + 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 is written before the sleep and therefore - survives SIGKILL) and is then force-terminated by SIGKILL (proven by the - absence of the `sigkill_not_received` file, which would only exist had the process - been allowed to finish sleeping). Both assertions are checked in - control_daemon_mock after the "Startup" transition succeeds. + `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") - lm_process = run_until_file_deployed( + run_until_file_deployed( target=target, binary_path=str(remote_test_dir / "launch_manager"), file_path=remote_test_dir.parent / "test_end", @@ -55,11 +53,4 @@ def test_shutdown_signal(target, setup_test, assert_test_results, remote_test_di timeout_s=10.0, ) - # shutdown_signal_process prints this line on a file system failure; guard - # against it explicitly so a write error can never masquerade as a SIGKILL. - assert "[FAILED] Failed to create file" not in lm_process.get_output(), ( - "shutdown_signal_process hit a filesystem error writing a marker file; " - "the sigkill_not_received assertion would be unreliable" - ) - assert_test_results({"control_daemon_mock.xml", "shutdown_signal_process.xml"}) diff --git a/tests/integration/shutdown_signal/shutdown_signal_process.cpp b/tests/integration/shutdown_signal/shutdown_signal_process.cpp index 6c42f803f2..ec3bb7c7e3 100644 --- a/tests/integration/shutdown_signal/shutdown_signal_process.cpp +++ b/tests/integration/shutdown_signal/shutdown_signal_process.cpp @@ -11,64 +11,49 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include -#include -#include -#include #include "common.hpp" #include "tests/utils/test_helper/test_helper.hpp" +#include +#include #include +#include +#include namespace { -/// @brief How long the process sleeps after receiving SIGTERM. This must be -/// clearly larger than the configured shutdown_timeout so that the process does -/// not terminate itself in time, forcing the Launch Manager to send SIGKILL. -constexpr unsigned int kSleepAfterSigtermSeconds = 5U; - -/// @brief Creates an empty file using only async-signal-safe calls so it is safe -/// to invoke from within a signal handler. -void createFileAsyncSignalSafe(const std::string_view path) -{ - const int fd = open(path.data(), O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd < 0) - { - // write()/_exit() are async-signal-safe; std::cerr/std::exit are not. - static constexpr char prefix[] = "[FAILED] Failed to create file "; - static constexpr char suffix[] = " in signal handler\n"; - static_cast(write(STDERR_FILENO, prefix, sizeof(prefix) - 1)); - static_cast(write(STDERR_FILENO, path.data(), path.size())); - static_cast(write(STDERR_FILENO, suffix, sizeof(suffix) - 1)); - static_cast(unlink(path.data())); // leave no partial file - _exit(-1); - } - static_cast(close(fd)); -} - /// @brief SIGTERM handler installed by the process under test. /// -/// It records that a SIGTERM was received (so this can be verified even after -/// the process is gone, since no code runs after SIGKILL) and then deliberately -/// sleeps past the configured shutdown_timeout instead of terminating. This -/// forces the Launch Manager to escalate to SIGKILL. If the sleep ever returns -/// (i.e. SIGKILL did not arrive), a second file is written to flag the failure. +/// 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*/) { - createFileAsyncSignalSafe(sigterm_received_file); - - // Do NOT terminate: outlast the shutdown_timeout so SIGKILL is required. - static_cast(sleep(kSleepAfterSigtermSeconds)); + // 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(write(fd, &pid, sizeof(pid))); + static_cast(close(fd)); + } - // Reaching this point means we were not SIGKILLed - record graceful exit so - // the assertion in control_daemon_mock can detect that SIGKILL did not work. - createFileAsyncSignalSafe(sigkill_not_received_file); + // Do NOT terminate: block until SIGKILL arrives so shutdown requires it. + while (true) + { + static_cast(pause()); + } } } // namespace TEST(ShutdownSignal, Process) { - // Remove any leftover files from a previous manual run. - ASSERT_TRUE(check_clean({sigterm_received_file, sigkill_not_received_file}, false)); + // 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