Skip to content
Open
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
58 changes: 58 additions & 0 deletions tests/integration/lm_sigkill_children_survive/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# *******************************************************************************
# 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 = "lm_sigkill_children_survive_common",
hdrs = ["common.hpp"],
deps = [
"@googletest//:gtest_main",
],
)

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

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

integration_test(
name = "lm_sigkill_children_survive",
timeout = "short",
srcs = ["lm_sigkill_children_survive.py"],
binaries = [
":control_client_test_driver",
":application_process",
"//score/launch_manager",
],
config = ":lm_sigkill_children_survive.json",
)
Original file line number Diff line number Diff line change
@@ -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
********************************************************************************/
#include <gtest/gtest.h>

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

// Application process started by the Launch Manager. It publishes its PID and
// reports running, then blocks (in the TestRunner destructor) so it stays alive
// after the Launch Manager is killed.
TEST(LmSigkillChildrenSurvive, ApplicationProcess)
{
// Publish our PID before reporting running: once the "Running" run target is
// active the test is guaranteed to find this file.
ASSERT_TRUE(write_pid(app_pid_file));

score::mw::lifecycle::report_running();
}

int main()
{
return TestRunner(__FILE__).RunTests();
}
44 changes: 44 additions & 0 deletions tests/integration/lm_sigkill_children_survive/common.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/********************************************************************************
* 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_LM_SIGKILL_CHILDREN_SURVIVE_COMMON_HPP
#define SCORE_TESTS_INTEGRATION_LM_SIGKILL_CHILDREN_SURVIVE_COMMON_HPP

#include <gtest/gtest.h>
#include <unistd.h>
#include <fstream>
#include <string>
#include <string_view>

/// @brief PID files written by the managed processes (as decimal text) so the
/// test can track them by PID after the Launch Manager has been SIGKILLed.
constexpr std::string_view daemon_pid_file = "daemon_pid";
constexpr std::string_view app_pid_file = "app_pid";

/// @brief Touched by the control daemon once both managed processes are running,
/// signalling the test that it may SIGKILL the Launch Manager.
constexpr std::string_view children_ready_file = "children_ready";

/// @brief Writes the current PID as decimal text to @p path.
/// @return AssertionSuccess if the PID was written successfully.
inline testing::AssertionResult write_pid(const std::string_view path)
{
std::ofstream out{std::string{path}, std::ios::trunc};
out << getpid();
if (!out)
{
return testing::AssertionFailure() << "Failed to write PID to " << path;
}
return testing::AssertionSuccess();
}

#endif // SCORE_TESTS_INTEGRATION_LM_SIGKILL_CHILDREN_SURVIVE_COMMON_HPP
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/********************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/
#include <gtest/gtest.h>

#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>

// Control daemon: starts the application process by activating the "Running" run
// target, then publishes readiness and blocks. It never switches away from
// "Running", so both it and the application process are still children of the
// Launch Manager when the test SIGKILLs it.
TEST(LmSigkillChildrenSurvive, ControlDaemon)
{
score::mw::lifecycle::ControlClient client{};

ASSERT_TRUE(check_clean({daemon_pid_file, app_pid_file, children_ready_file}, /*strict=*/false));

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);
ASSERT_TRUE(result.has_value()) << "Activating target Running failed: " << result.error().Message();
}

// Publish our own PID and signal the test, which then SIGKILLs the Launch Manager.
TEST_STEP("Signal readiness")
{
ASSERT_TRUE(write_pid(daemon_pid_file));
ASSERT_TRUE(touch_file(children_ready_file));
}
}

int main()
{
return TestRunner(__FILE__).RunTests();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
{
"schema_version": 1,
"defaults": {
"deployment_config": {
"bin_dir": "/tmp/tests/lm_sigkill_children_survive",
"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_client_test_driver",
"application_profile": {
"application_type": "State_Manager",
"alive_supervision": {
"min_indications": 0
}
}
},
"deployment_config": {
"ready_timeout": 1.0,
"shutdown_timeout": 1.0,
"environmental_variables": {
"PROCESSIDENTIFIER": "control_daemon"
}
}
},
"application_process": {
"component_properties": {
"binary_name": "application_process",
"application_profile": {
"application_type": "Reporting"
}
},
"deployment_config": {
"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",
"application_process"
],
"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": []
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# *******************************************************************************
# 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


def _read_pid(target, pid_file):
code, out = target.execute(f"cat {pid_file}")
assert code == 0, f"Failed to read {pid_file}: {out!r}"
return int(out.decode().strip())


def _pid_alive(target, pid):
return target.execute(f"kill -0 {pid}")[0] == 0


@add_test_properties(
partially_verifies=[],
fully_verifies=["comp_req__launch_man__fast_shutdown_support"],
test_type="interface-test",
derivation_technique="explorative-testing",
)
def test_lm_sigkill_children_survive(
target, setup_test, assert_test_results, remote_test_dir
):
"""
Objective: Verifies that processes started by the Launch Manager keep running
when the Launch Manager itself is killed with SIGKILL, i.e. without any chance
to tear its children down.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the fast shutdown should be a sigkill, in that case I'm not sure if there is anything to test really as the LCM doesn't really have anything to say about getting killed.
In that case I also don't know if we should even have a requirement for this, as there is no code we write that changes the behavior.
But if this improves the component req coverage then I guess we might as well test it?

I think we need to clarify why the req is required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, we can do so again today.


The control daemon activates the "Running" run target, which starts the managed
application process, and then signals readiness. The test SIGKILLs only the
Launch Manager and checks that both the control daemon and the application
process are still alive afterwards.

Expected Behaviour: After the Launch Manager is SIGKILLed, both child processes
remain running.
"""

config_path = str(remote_test_dir / "etc/lm_sigkill_children_survive.bin")
ready_file = remote_test_dir / "children_ready"
daemon_pid_file = remote_test_dir / "daemon_pid"
app_pid_file = remote_test_dir / "app_pid"

# Both children are up and reporting once the daemon touches the ready file.
# Keep the launch manager running so the test can SIGKILL it below.
proc = run_until_file_deployed(
target=target,
binary_path=str(remote_test_dir / "launch_manager"),
file_path=ready_file,
cwd=str(remote_test_dir),
args=["-c", config_path],
timeout_s=5.0,
stop_on_file=False,
)

daemon_pid = None
app_pid = None
try:
daemon_pid = _read_pid(target, daemon_pid_file)
app_pid = _read_pid(target, app_pid_file)

# Kill the Launch Manager - and only the Launch Manager - via SIGKILL.
assert proc.is_running(), "Launch manager exited before it could be killed"
code, out = target.execute(f"kill -9 {proc.pid()}")
assert code == 0, f"Failed to SIGKILL launch manager (pid {proc.pid()}): {out!r}"
proc.wait(timeout_s=3.0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really the intended implementation of "fast shutdown" by killing the launch manager?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least this is how I have understood it. This is what I have noted:

Some kind of brute force shutdown. (May be) related to qnx feature which is watching the running processes, if launch manager is gone, system gets restarted anyways. This is most probably faster then stop triggering the watchdog and waiting for the timeout which initiates the system restart.


# The child processes must survive the death of their parent.
assert _pid_alive(target, daemon_pid), (
f"Control daemon (pid {daemon_pid}) died with the launch manager"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we test that they survive? Not sure it's relevant for the requirement.

)
assert _pid_alive(target, app_pid), (
f"Application process (pid {app_pid}) died with the launch manager"
)

assert_test_results(
{"control_client_test_driver.xml", "application_process.xml"}
)
finally:
# Clean up the now-orphaned children so they do not leak on the target.
for pid in (daemon_pid, app_pid):
if pid is not None:
target.execute(f"kill -9 {pid}")
Loading
Loading