From 393f902c211fa098299a3b97ab941d1351b79adc Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 20 Jul 2026 17:15:29 +0200 Subject: [PATCH 01/20] Add stub for test sandbox options Copied smoke test and renamed accordingly. --- tests/integration/sandbox_options/BUILD | 53 ++++++++ .../sandbox_options/control_daemon_mock.cpp | 53 ++++++++ .../sandbox_options/sandbox_options.json | 124 ++++++++++++++++++ .../sandbox_options/sandbox_options.py | 53 ++++++++ .../sandbox_options/sandbox_process.cpp | 30 +++++ 5 files changed, 313 insertions(+) create mode 100644 tests/integration/sandbox_options/BUILD create mode 100644 tests/integration/sandbox_options/control_daemon_mock.cpp create mode 100644 tests/integration/sandbox_options/sandbox_options.json create mode 100644 tests/integration/sandbox_options/sandbox_options.py create mode 100644 tests/integration/sandbox_options/sandbox_process.cpp diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD new file mode 100644 index 000000000..afb20b491 --- /dev/null +++ b/tests/integration/sandbox_options/BUILD @@ -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 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_binary") +load("//tests/utils/bazel:integration.bzl", "integration_test") + +exports_files( + ["lm_demo.json"], + visibility = ["//examples:__subpackages__"], +) + +cc_binary( + name = "control_daemon_mock", + srcs = ["control_daemon_mock.cpp"], + data = [], + deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "sandbox_process", + srcs = ["sandbox_process.cpp"], + deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +integration_test( + name = "smoke", + srcs = ["smoke.py"], + binaries = [ + ":control_daemon_mock", + ":sandbox_process", + "//score/launch_manager", + ], + config = ":lifecycle_smoketest.json", +) diff --git a/tests/integration/sandbox_options/control_daemon_mock.cpp b/tests/integration/sandbox_options/control_daemon_mock.cpp new file mode 100644 index 000000000..7e6484bf9 --- /dev/null +++ b/tests/integration/sandbox_options/control_daemon_mock.cpp @@ -0,0 +1,53 @@ +/******************************************************************************** + * Copyright (c) 2025 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 "tests/utils/test_helper/test_helper.hpp" +#include +#include + +TEST(Smoke, Daemon) +{ + score::mw::lifecycle::ControlClient client{}; + ASSERT_TRUE(check_clean({test_end_location})); + TEST_STEP("Control daemon report running") + { + // 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(); + } + TEST_STEP("Activate RunTarget Startup") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("Startup").Get(stop_token); + EXPECT_TRUE(result.has_value()); + } + TEST_STEP("Activate RunTarget Off") + { + client.ActivateRunTarget("Off"); + } +} + +int main(int argc, char** argv) +{ + return TestRunner(__FILE__, TerminationBehavior::kWait, TerminationNotification::kTestEnd).RunTests(); +} diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json new file mode 100644 index 000000000..a78ba7f49 --- /dev/null +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -0,0 +1,124 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/sandbox_options", + "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" + } + } + }, + "sandbox_process": { + "component_properties": { + "binary_name": "sandbox_process", + "working_dir": "/tmp/sandbox/test", + "application_profile": { + "application_type": "Reporting" + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "DefaultPG_app0" + }, + "sandbox": { + "uid": 666, + "gid": 666, + "supplementary_group_ids": [123, 321, 456], + "scheduling_policy": "SCHED_FIFO", + "scheduling_priority": 10 + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_daemon" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Running": { + "depends_on": [ + "control_daemon", + "sandbox_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/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py new file mode 100644 index 000000000..373323463 --- /dev/null +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -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 +# ******************************************************************************* +import logging +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=[ + "feat_req__lifecycle__uid_gid_support", + "feat_req__lifecycle__launch_priority_support", + "feat_req__lifecycle__cwd_support", + "feat_req__lifecycle__supplementary_groups" + ], + partially_verifies=[], + test_type="interface-test", + derivation_technique="explorative-testing", +) +def test_sandbox_options(target, setup_test, assert_test_results, remote_test_dir): + """ + Objective: Verifies the effectiveness of sandbox-options as gid, uid, supplementary groups, and scheduling policy. + + The launch manager starts with an initial run target. The control daemon activates the "Running" run target (starting the managed process with the sandbox options applied), then transitions back to "Startup", and finally activates "Off". + Expected Behaviour: All run target transitions complete successfully and all processes report running. + """ + + # 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/sandbox_options.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=3.0, + ) + + assert_test_results({"control_daemon_mock.xml", "gtest_process.xml"}) diff --git a/tests/integration/sandbox_options/sandbox_process.cpp b/tests/integration/sandbox_options/sandbox_process.cpp new file mode 100644 index 000000000..3b7649ba1 --- /dev/null +++ b/tests/integration/sandbox_options/sandbox_process.cpp @@ -0,0 +1,30 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/test_helper.hpp" + +TEST(SandboxOptions, RunAndVerify) { + // report running + score::mw::lifecycle::report_running(); + + +} + +int main() { + return TestRunner(__FILE__).RunTests(); +} From a254a36de37beacd5f04aebc1642adf060c317ac Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Tue, 21 Jul 2026 09:25:31 +0200 Subject: [PATCH 02/20] First attempt with opencode --- .../configuration/configuration_adapter.cpp | 1 + .../details/process_launcher.cpp | 10 ++ .../src/process_group_manager/iprocess.hpp | 1 + tests/integration/sandbox_options/BUILD | 12 +- .../sandbox_options/sandbox_options.json | 10 +- .../sandbox_options/sandbox_options.py | 2 +- .../sandbox_options_process.cpp | 112 ++++++++++++++++++ .../sandbox_options/sandbox_process.cpp | 30 ----- 8 files changed, 136 insertions(+), 42 deletions(-) create mode 100644 tests/integration/sandbox_options/sandbox_options_process.cpp delete mode 100644 tests/integration/sandbox_options/sandbox_process.cpp diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp index 4b2ecd3c3..5f9ce161c 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp @@ -77,6 +77,7 @@ void ConfigurationAdapter::fillStartupConfigFromDeployment( const auto& props = comp.component_properties; startup.executable_path_ = deploy.bin_dir + "/" + props.binary_name; + startup.working_dir_ = deploy.working_dir; startup.short_name_ = comp.name; startup.uid_ = deploy.sandbox.uid; diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp index 1b7369578..7cc48c772 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp @@ -133,6 +133,16 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) void changeCurrentWorkingDirectory(const score::lcm::internal::osal::OsalConfig& config) { + if (!config.working_dir_.empty()) + { + if (-1 == chdir(config.working_dir_.c_str())) + { + LM_LOG_ERROR() << "[New process] chdir(" << config.working_dir_ << ") failed:" << score::lcm::internal::errno_message(errno); + sysexit(EXIT_FAILURE); + } + return; + } + // Change current working directory to the same as the executable constexpr size_t string_size = static_cast(PATH_MAX); // Notice that this next static variable is duplicated by the fork() and so does not need diff --git a/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp b/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp index e50738951..be7a9f5fd 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp @@ -67,6 +67,7 @@ struct OsalConfig int32_t scheduling_policy_; ///< Scheduling policy defined for this process int32_t scheduling_priority_; ///< Scheduling priority for this process OsalLimits resource_limits_; ///< Resource limits for this process + std::string working_dir_{}; ///< Working directory for the process }; /// @brief Struct to hold configuration parameters for the child process. diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index afb20b491..08bba667a 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -31,8 +31,8 @@ cc_binary( ) cc_binary( - name = "sandbox_process", - srcs = ["sandbox_process.cpp"], + name = "sandbox_options_process", + srcs = ["sandbox_options_process.cpp"], deps = [ "//score/launch_manager:control_cc", "//score/launch_manager:lifecycle_cc", @@ -42,12 +42,12 @@ cc_binary( ) integration_test( - name = "smoke", - srcs = ["smoke.py"], + name = "sandbox_options", + srcs = ["sandbox_options.py"], binaries = [ ":control_daemon_mock", - ":sandbox_process", + ":sandbox_options_process", "//score/launch_manager", ], - config = ":lifecycle_smoketest.json", + config = ":sandbox_options.json", ) diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index a78ba7f49..38dcfeac3 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -60,10 +60,9 @@ } } }, - "sandbox_process": { + "sandbox_options_process": { "component_properties": { - "binary_name": "sandbox_process", - "working_dir": "/tmp/sandbox/test", + "binary_name": "sandbox_options_process", "application_profile": { "application_type": "Reporting" } @@ -78,7 +77,8 @@ "supplementary_group_ids": [123, 321, 456], "scheduling_policy": "SCHED_FIFO", "scheduling_priority": 10 - } + }, + "working_dir": "/tmp" } } }, @@ -96,7 +96,7 @@ "Running": { "depends_on": [ "control_daemon", - "sandbox_process" + "sandbox_options_process" ], "recovery_action": { "switch_run_target": { diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 373323463..7de834c62 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -50,4 +50,4 @@ def test_sandbox_options(target, setup_test, assert_test_results, remote_test_di timeout_s=3.0, ) - assert_test_results({"control_daemon_mock.xml", "gtest_process.xml"}) + assert_test_results({"control_daemon_mock.xml", "sandbox_options_process.xml"}) diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp new file mode 100644 index 000000000..56d456728 --- /dev/null +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -0,0 +1,112 @@ +/******************************************************************************** + * 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 +#include +#include + +#include +#include "tests/utils/test_helper/test_helper.hpp" + +namespace { + +bool verify_sandbox_options() +{ + bool all_pass = true; + + const uid_t expected_uid = 666; + const gid_t expected_gid = 666; + const std::array expected_supp_groups = {123, 321, 456}; + const std::string expected_cwd = "/tmp"; + + TEST_STEP("Verify uid and gid") + { + const uid_t current_uid = getuid(); + const gid_t current_gid = getgid(); + + std::ofstream uid_file{"sandbox_uid.txt"}; + uid_file << current_uid; + std::ofstream gid_file{"sandbox_gid.txt"}; + gid_file << current_gid; + + EXPECT_EQ(current_uid, expected_uid) << "Expected uid=" << expected_uid << " but got uid=" << current_uid; + EXPECT_EQ(current_gid, expected_gid) << "Expected gid=" << expected_gid << " but got gid=" << current_gid; + + if (current_uid != expected_uid || current_gid != expected_gid) + { + all_pass = false; + } + } + + TEST_STEP("Verify supplementary groups") + { + std::vector groups(256); + const int count = getgroups(static_cast(groups.size()), groups.data()); + EXPECT_GE(count, 0) << "Failed to get supplementary groups"; + + std::ofstream supp_file{"sandbox_supp_groups.txt"}; + for (int i = 0; i < count; ++i) + { + supp_file << groups[i] << (i < count - 1 ? "," : ""); + } + + for (const gid_t expected_group : expected_supp_groups) + { + const bool found = std::find(groups.begin(), groups.begin() + count, expected_group) != groups.end(); + EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" + << count << " total)]"; + if (!found) + { + all_pass = false; + } + } + } + + TEST_STEP("Verify working directory") + { + char buf[PATH_MAX]; + char* result = getcwd(buf, sizeof(buf)); + EXPECT_NE(result, nullptr) << "Failed to get current working directory"; + + if (result) + { + std::ofstream cwd_file{"sandbox_cwd.txt"}; + cwd_file << result; + + EXPECT_EQ(std::string(result), expected_cwd) + << "Expected working_dir=" << expected_cwd << " but got cwd=" << result; + + if (std::string(result) != expected_cwd) + { + all_pass = false; + } + } + } + + return all_pass; +} + +} // namespace + +TEST(SandboxOptions, RunAndVerify) { + ASSERT_TRUE(verify_sandbox_options()) << "Sandbox options verification failed"; + score::mw::lifecycle::report_running(); +} + +int main() { + return TestRunner(__FILE__).RunTests(); +} diff --git a/tests/integration/sandbox_options/sandbox_process.cpp b/tests/integration/sandbox_options/sandbox_process.cpp deleted file mode 100644 index 3b7649ba1..000000000 --- a/tests/integration/sandbox_options/sandbox_process.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/******************************************************************************** - * 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 "tests/utils/test_helper/test_helper.hpp" - -TEST(SandboxOptions, RunAndVerify) { - // report running - score::mw::lifecycle::report_running(); - - -} - -int main() { - return TestRunner(__FILE__).RunTests(); -} From 6f88fba9fafc75f3ee669076279d845a0b4c3b10 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Tue, 21 Jul 2026 11:30:06 +0200 Subject: [PATCH 03/20] Patch the docker environment Allow execution of sched_setscheduler call by adding SYS_NICE capability to the docker environment --- MODULE.bazel | 10 ++++++++++ patches/BUILD | 17 +++++++++++++---- patches/score_itf_docker_cap_add.patch | 11 +++++++++++ .../sandbox_options/sandbox_options.py | 10 ++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 patches/score_itf_docker_cap_add.patch diff --git a/MODULE.bazel b/MODULE.bazel index adf6115c6..22322825f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -159,6 +159,16 @@ use_repo(pip, "score_lifecycle_pip") bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True) +# Patch the Docker plugin so tests can request extra container privileges +# (e.g. cap_add=["SYS_NICE"], needed for SCHED_FIFO/sched_setscheduler) via the +# `docker_configuration` fixture. Remove once forwarding lands upstream. +single_version_override( + module_name = "score_itf", + patch_strip = 1, + patches = ["//patches:score_itf_docker_cap_add.patch"], + version = "0.3.0", +) + oci = use_extension("@rules_oci//oci:extensions.bzl", "oci", dev_dependency = True) oci.pull( name = "debian-test-runtime", diff --git a/patches/BUILD b/patches/BUILD index 42ef274e6..35f6e0696 100644 --- a/patches/BUILD +++ b/patches/BUILD @@ -1,5 +1,14 @@ -exports_files(["hedron_compile_commands_env_vars.patch"]) +# ******************************************************************************* +# 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 +# ******************************************************************************* -exports_files(["score_bazel_tools.patch"]) - -exports_files(["score_bazel_tools_python.patch"]) +exports_files(glob(["*.patch"])) diff --git a/patches/score_itf_docker_cap_add.patch b/patches/score_itf_docker_cap_add.patch new file mode 100644 index 000000000..f2a372799 --- /dev/null +++ b/patches/score_itf_docker_cap_add.patch @@ -0,0 +1,11 @@ +--- a/score/itf/plugins/docker.py ++++ b/score/itf/plugins/docker.py +@@ -331,6 +331,8 @@ def target_init(request, _docker_configuration): + environment=_docker_configuration["environment"], + volumes=_docker_configuration["volumes"], + shm_size=_docker_configuration["shm_size"], ++ privileged=_docker_configuration.get("privileged", False), ++ cap_add=_docker_configuration.get("cap_add"), + ) + target = None + try: diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 7de834c62..1cdadc6f2 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -11,12 +11,22 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* import logging +import pytest +from score.itf.plugins.core import determine_target_scope 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 +@pytest.fixture(scope=determine_target_scope) +def docker_configuration(): + """Grant the container CAP_SYS_NICE so the launch manager can apply the + SCHED_FIFO real-time scheduling policy configured in sandbox_options.json. + Without it, sched_setscheduler() fails with 'Operation not permitted'.""" + return {"cap_add": ["SYS_NICE"]} + + @add_test_properties( fully_verifies=[ "feat_req__lifecycle__uid_gid_support", From d78fca5b0fdf6394136e093cce2a68cced1fade2 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 15:00:29 +0200 Subject: [PATCH 04/20] Update known_limitations.rst --- .../docs/product_documentation/known_limitations.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/score/launch_manager/docs/product_documentation/known_limitations.rst b/score/launch_manager/docs/product_documentation/known_limitations.rst index 4cfc8e935..6fbcca996 100644 --- a/score/launch_manager/docs/product_documentation/known_limitations.rst +++ b/score/launch_manager/docs/product_documentation/known_limitations.rst @@ -33,8 +33,7 @@ Component ``run_target`` set to ``fallback_run_target``. * The ``ready_timeout`` is used as the timeout until process state Running is reached, even in case the ReadyCondition is ``process_state:Terminated``. -* The parameter ``deployment_config/working_dir`` is currently not supported - and is ignored. + Run target ---------- From 36e549c98ba9ffc2f4257929246c4ff01862f27e Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 16:34:40 +0200 Subject: [PATCH 05/20] Finish support for setting the working dir Some unit tests are currently broken however. --- .../config_schema/lm_flatcfg.fbs | 1 + .../configuration/configuration_manager.cpp | 4 +- scripts/config_mapping/lifecycle_config.py | 11 +++- .../sandbox_options/control_daemon_mock.cpp | 3 +- .../sandbox_options/sandbox_options.py | 8 ++- .../sandbox_options_process.cpp | 23 ++++--- tests/utils/testing_utils/test_results.py | 61 +++++++++++++------ 7 files changed, 76 insertions(+), 35 deletions(-) diff --git a/score/launch_manager/src/daemon/src/configuration/config_schema/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/config_schema/lm_flatcfg.fbs index e7f6fad15..ae687008d 100644 --- a/score/launch_manager/src/daemon/src/configuration/config_schema/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/config_schema/lm_flatcfg.fbs @@ -43,6 +43,7 @@ table Process { uid: uint (id:4); gid: uint (id:5); path: string (id:6); + working_dir: string (id:11); security_policy_details: string (id:7); executable_reporting_behavior: ExecutionStateReportingBehaviorEnum (id:8); startup_config: [ProcessStartupConfig] (id:9); diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_manager.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_manager.cpp index bc538918c..4ce25de8b 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_manager.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_manager.cpp @@ -521,8 +521,10 @@ bool ConfigurationManager::parseProcessConfigurations(const Process* node) setSchedulingParameters(*node, *startup_config_node, instance); // Set executable path from node's path instance.startup_config_.executable_path_ = getStringFromFlatBuffer(node->path()); + instance.startup_config_.working_dir_ = getStringFromFlatBuffer(node->working_dir()); LM_LOG_DEBUG() << "parseProcessConfigurations: Process index:" << instance.process_number_ - << "executable_path_:" << instance.startup_config_.executable_path_; + << "executable_path_:" << instance.startup_config_.executable_path_ + << " working_dir_:" << instance.startup_config_.working_dir_; instance.startup_config_.short_name_ = node->identifier() ? node->identifier()->c_str() : "Unknown"; instance.startup_config_.uid_ = node->uid() & 0x7FFFFFFFU; diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 680ac4b1f..0bcebde10 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -27,7 +27,6 @@ "shutdown_timeout": 0.5, "environmental_variables": {}, "bin_dir": "/opt", - "working_dir": "/tmp", "ready_recovery_action": { "restart": { "number_of_attempts": 0, @@ -289,7 +288,11 @@ def gen_config(output_dir, config, input_filename, schema_version=None): "ready_timeout": depl_cfg["ready_timeout"], "shutdown_timeout": depl_cfg["shutdown_timeout"], "bin_dir": depl_cfg["bin_dir"], - "working_dir": depl_cfg["working_dir"], + # Default the working directory to bin_dir (the directory the + # executable lives in) when not set explicitly. This matches the + # launch manager's own fallback of chdir'ing to the executable's + # directory when no working_dir is configured. + "working_dir": depl_cfg.get("working_dir", depl_cfg["bin_dir"]), "sandbox": sandbox_out, } @@ -668,6 +671,10 @@ def get_terminating_behavior(component_config): process["number_of_restart_attempts"] = component_config["deployment_config"][ "ready_recovery_action" ]["restart"]["number_of_attempts"] + process["working_dir"] = component_config["deployment_config"].get( + "working_dir", + component_config["deployment_config"]["bin_dir"] + ) match component_config["component_properties"]["application_profile"][ "application_type" diff --git a/tests/integration/sandbox_options/control_daemon_mock.cpp b/tests/integration/sandbox_options/control_daemon_mock.cpp index 7e6484bf9..d0c684303 100644 --- a/tests/integration/sandbox_options/control_daemon_mock.cpp +++ b/tests/integration/sandbox_options/control_daemon_mock.cpp @@ -25,7 +25,6 @@ TEST(Smoke, Daemon) ASSERT_TRUE(check_clean({test_end_location})); TEST_STEP("Control daemon report running") { - // report running score::mw::lifecycle::report_running(); } @@ -47,7 +46,7 @@ TEST(Smoke, Daemon) } } -int main(int argc, char** argv) +int main() { return TestRunner(__FILE__, TerminationBehavior::kWait, TerminationNotification::kTestEnd).RunTests(); } diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 1cdadc6f2..1828f9c55 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -60,4 +60,10 @@ def test_sandbox_options(target, setup_test, assert_test_results, remote_test_di timeout_s=3.0, ) - assert_test_results({"control_daemon_mock.xml", "sandbox_options_process.xml"}) + # The managed process is configured with a custom working directory (see the + # "working_dir" entries in sandbox_options.json), so its XML result file is + # written there rather than into remote_test_dir. Search those directories too. + assert_test_results( + {"control_daemon_mock.xml", "sandbox_options_process.xml"}, + additional_search_dirs=["/tmp/tests/sandbox_options", "/tmp"], + ) diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index 56d456728..eac5a95b8 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -12,17 +12,18 @@ ********************************************************************************/ #include -#include #include #include +#include #include +#include #include -#include -#include #include "tests/utils/test_helper/test_helper.hpp" +#include -namespace { +namespace +{ bool verify_sandbox_options() { @@ -67,8 +68,8 @@ bool verify_sandbox_options() for (const gid_t expected_group : expected_supp_groups) { const bool found = std::find(groups.begin(), groups.begin() + count, expected_group) != groups.end(); - EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" - << count << " total)]"; + EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" << count + << " total)]"; if (!found) { all_pass = false; @@ -100,13 +101,15 @@ bool verify_sandbox_options() return all_pass; } -} // namespace +} // namespace -TEST(SandboxOptions, RunAndVerify) { +TEST(SandboxOptions, RunAndVerify) +{ ASSERT_TRUE(verify_sandbox_options()) << "Sandbox options verification failed"; score::mw::lifecycle::report_running(); } -int main() { +int main() +{ return TestRunner(__FILE__).RunTests(); -} +} \ No newline at end of file diff --git a/tests/utils/testing_utils/test_results.py b/tests/utils/testing_utils/test_results.py index 15934db40..15e097a97 100644 --- a/tests/utils/testing_utils/test_results.py +++ b/tests/utils/testing_utils/test_results.py @@ -11,30 +11,45 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* from pathlib import Path -from typing import Set +from typing import Iterable, Sequence, Set, Union import pytest from junitparser import JUnitXml -def download_xml_results(target, remote_dir: Path, local_dir: Path): - """Glob all .xml files in remote_dir, download them to local_dir, and delete - them from the remote. +def download_xml_results( + target, remote_dirs: Union[Path, str, Sequence[Union[Path, str]]], local_dir: Path +): + """Glob all .xml files in each of `remote_dirs`, download them to `local_dir`, + and delete them from the remote. + + `remote_dirs` may be a single directory or an iterable of directories. Each + directory is searched at most once, even if listed multiple times. """ - res, stdout = target.execute(f"ls {remote_dir}/*.xml") - assert res == 0, "Couldn't get list of files" - remote_xml_files = stdout.decode().strip().splitlines() - for remote_path in remote_xml_files: - remote_path = remote_path.strip() - if not remote_path: + if isinstance(remote_dirs, (str, Path)): + remote_dirs = [remote_dirs] + + seen: Set[str] = set() + for remote_dir in remote_dirs: + remote_dir = str(remote_dir) + if remote_dir in seen: continue - xml_name = Path(remote_path).name - try: - target.download(remote_path, str(local_dir / xml_name)) - target.execute(f"rm {remote_path}") - except Exception: - pass + seen.add(remote_dir) + res, stdout = target.execute(f"ls {remote_dir}/*.xml 2>/dev/null") + if res != 0: + continue + remote_xml_files = stdout.decode().strip().splitlines() + for remote_path in remote_xml_files: + remote_path = remote_path.strip() + if not remote_path: + continue + xml_name = Path(remote_path).name + try: + target.download(remote_path, str(local_dir / xml_name)) + target.execute(f"rm {remote_path}") + except Exception: + pass def get_failing_files(path: Path): """Collects all produced xml files and returns that set as well as the subset of failing files""" @@ -55,17 +70,25 @@ def assert_test_results(target, remote_test_dir, test_output_dir): count with no failures. Takes `target`, `remote_test_dir`, and `test_output_dir` from fixtures automatically. + By default only `remote_test_dir` is searched for XML result files. Tests + that configure processes with a different working directory can pass + `additional_search_dirs` so those locations are searched as well. + Usage:: def test_foo(assert_test_results, ...): ... - assert_test_results(expected_count=2) + assert_test_results({"foo.xml"}) + assert_test_results({"foo.xml"}, additional_search_dirs=["/tmp"]) """ - def _assert(expected_xml_files: Set[str]): + def _assert( + expected_xml_files: Set[str], additional_search_dirs: Iterable[Union[Path, str]] = () + ): # Show the error as coming from the call in the test rather than here __tracebackhide__ = True - download_xml_results(target, remote_test_dir, test_output_dir) + search_dirs = [remote_test_dir, *additional_search_dirs] + download_xml_results(target, search_dirs, test_output_dir) all_files, failing_files = get_failing_files(test_output_dir) assert all_files == expected_xml_files, ( # Set equality f"Didn't find the expected files, found: {all_files}" From 3ef7843f373b6e0260a49f95717ca224239bda09 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 16:39:12 +0200 Subject: [PATCH 06/20] Stabilize utility function --- .../testing_utils/run_until_file_deployed.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/utils/testing_utils/run_until_file_deployed.py b/tests/utils/testing_utils/run_until_file_deployed.py index eb7d87f29..5eda22027 100644 --- a/tests/utils/testing_utils/run_until_file_deployed.py +++ b/tests/utils/testing_utils/run_until_file_deployed.py @@ -25,6 +25,7 @@ def run_until_file_deployed( file_path: str, timeout_s: float = 30.0, poll_interval_s: float = 0.5, + stop_timeout_s: float = 2.0, args=None, cwd: str = "/", ) -> AsyncProcess: @@ -35,6 +36,8 @@ def run_until_file_deployed( :param file_path: path of the file to wait for on the target. :param timeout_s: maximum seconds to wait for the file (default: 30). :param poll_interval_s: seconds between file-existence checks (default: 0.5). + :param stop_timeout_s: maximum seconds to wait for the process to terminate + after SIGTERM (default: 2). :param args: optional list of arguments to pass to the binary. :param cwd: working directory on the target (default: "/"). :return: the stopped :class:`AsyncProcess` handle. @@ -64,12 +67,21 @@ def run_until_file_deployed( # run their cleanup code before exiting. kill_cmd = f"kill -15 -{proc.pid()}" res, _ = target.execute(kill_cmd) - time.sleep(0.5) - assert proc.is_running() == False, "LCM still running" + assert res == 0, "Couldn't kill lcm" + + # Poll for the process to actually terminate instead of assuming a + # single fixed grace period is enough. Termination timing varies + # with system load, so a fixed sleep is flaky. + stop_deadline = time.monotonic() + stop_timeout_s + while proc.is_running() and time.monotonic() < stop_deadline: + time.sleep(poll_interval_s) + + assert not proc.is_running(), ( + f"LCM still running {stop_timeout_s}s after SIGTERM" + ) assert proc.get_exit_code() == 0, ( f"LCM did not exit cleanly, it died with code {proc.get_exit_code()}" ) - assert res == 0, "Couldn't kill lcm" return proc logger.debug(f"Waiting for {file_path}") From 875a71071766a066b220b024e955cbca5001f699 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 17:05:06 +0200 Subject: [PATCH 07/20] Fix config mapping tests --- scripts/config_mapping/integration_tests.py | 1 + scripts/config_mapping/lifecycle_config.py | 10 +++++++--- .../tests/basic_test/expected_output/lm_demo.json | 5 +++++ .../tests/lm_config_test/expected_output/lm_demo.json | 6 ++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index b393a6ac1..0e147a56f 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -28,6 +28,7 @@ script_dir.parent.parent / "score" / "launch_manager" + / "src" / "daemon" / "src" / "configuration" diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 0bcebde10..49b467655 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -184,9 +184,13 @@ def dict_merge_recursive(dict_a, dict_b): merged_defaults["watchdog"], config.get("watchdog", {}) ) - new_config["initial_run_target"] = config.get( - "initial_run_target", merged_defaults["initial_run_target"] - ) + # Only emit initial_run_target when it can be derived from the input config + # or the defaults. This keeps preprocess_defaults usable with minimal + # defaults that don't define run-target keys. + if "initial_run_target" in config or "initial_run_target" in merged_defaults: + new_config["initial_run_target"] = config.get( + "initial_run_target", merged_defaults.get("initial_run_target") + ) if "fallback_run_target" in config: fallback_defaults = { diff --git a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json index 660a30feb..ea0e5c0e5 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json @@ -20,6 +20,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -84,6 +85,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -152,6 +154,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -219,6 +222,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -295,6 +299,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "function_cluster_affiliation": "STATE_MANAGEMENT", "startup_config": [ diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json index c90079231..8b8e5a5fb 100644 --- a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json @@ -17,6 +17,7 @@ ], "security_policy_details": "policy_name_2", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -92,6 +93,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -161,6 +163,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -229,6 +232,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -296,6 +300,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -372,6 +377,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "function_cluster_affiliation": "STATE_MANAGEMENT", "startup_config": [ From afe9c252545f4bdbd896d5080888aaacd6010f5c Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 09:28:59 +0200 Subject: [PATCH 08/20] Add config mapping tests to the pipeline --- .github/workflows/test_and_docs.yml | 2 +- scripts/config_mapping/tests/BUILD | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_and_docs.yml b/.github/workflows/test_and_docs.yml index 8d790e9fd..ffd609a12 100644 --- a/.github/workflows/test_and_docs.yml +++ b/.github/workflows/test_and_docs.yml @@ -44,7 +44,7 @@ jobs: contents: read pull-requests: read with: - bazel-target: 'test --lockfile_mode=error //score/... //tests/... --config=x86_64-linux' + bazel-target: 'test --lockfile_mode=error //score/... //tests/... //scripts/... --config=x86_64-linux' upload-name: 'bazel-testlogs' packages: 'fakechroot' build-docs: diff --git a/scripts/config_mapping/tests/BUILD b/scripts/config_mapping/tests/BUILD index 9bf775d3c..825fd6e40 100644 --- a/scripts/config_mapping/tests/BUILD +++ b/scripts/config_mapping/tests/BUILD @@ -32,7 +32,6 @@ score_py_pytest( "//score/launch_manager/src/daemon/src/configuration/config_schema:launch_manager.schema.json", "//scripts/config_mapping:lifecycle_config.py", ], - tags = ["manual"], deps = [ requirement("pytest"), requirement("jsonschema"), From 84b46affc1ef9fcf512c6027619db954ce39c81c Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 14:30:23 +0200 Subject: [PATCH 09/20] Add check for scheduling priority / policy --- .../sandbox_options/sandbox_options.py | 1 + .../sandbox_options_process.cpp | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 1828f9c55..1019f8f81 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -31,6 +31,7 @@ def docker_configuration(): fully_verifies=[ "feat_req__lifecycle__uid_gid_support", "feat_req__lifecycle__launch_priority_support", + "feat_req__lifecycle__scheduling_policy", "feat_req__lifecycle__cwd_support", "feat_req__lifecycle__supplementary_groups" ], diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index eac5a95b8..2da3366de 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -12,12 +12,17 @@ ********************************************************************************/ #include +#include +#include #include #include #include #include +#include #include #include +#include +#include #include "tests/utils/test_helper/test_helper.hpp" #include @@ -25,6 +30,60 @@ namespace { +// Values configured for sandbox_options_process in sandbox_options.json. +constexpr int expected_policy = SCHED_FIFO; +constexpr int expected_priority = 10; + +const char* policy_name(const int policy) +{ + switch (policy) + { + case SCHED_FIFO: + return "SCHED_FIFO"; + case SCHED_RR: + return "SCHED_RR"; + case SCHED_OTHER: + return "SCHED_OTHER"; + default: + return "UNKNOWN"; + } +} + +/// @brief Verify that the calling thread runs with the expected scheduling policy and priority. +/// @param[in] context Human readable name of the thread, used in failure messages. +/// @param[in] file_prefix Prefix for the result files written for debugging. +/// @return true if the policy and priority match the configured values. +bool verify_scheduling(const std::string& context, const std::string& file_prefix) +{ + int policy = -1; + sched_param param{}; + const int rc = pthread_getschedparam(pthread_self(), &policy, ¶m); + EXPECT_EQ(rc, 0) << context << ": pthread_getschedparam failed rc=" << rc; + + std::ofstream policy_file{file_prefix + "_policy.txt"}; + policy_file << policy; + std::ofstream prio_file{file_prefix + "_priority.txt"}; + prio_file << param.sched_priority; + + bool pass = (rc == 0); + + EXPECT_EQ(policy, expected_policy) << context << ": Expected scheduling policy=" << policy_name(expected_policy) + << " but got " << policy_name(policy); + if (policy != expected_policy) + { + pass = false; + } + + EXPECT_EQ(param.sched_priority, expected_priority) + << context << ": Expected scheduling priority=" << expected_priority << " but got " << param.sched_priority; + if (param.sched_priority != expected_priority) + { + pass = false; + } + + return pass; +} + bool verify_sandbox_options() { bool all_pass = true; @@ -98,6 +157,30 @@ bool verify_sandbox_options() } } + TEST_STEP("Verify scheduling policy and priority in the main thread") + { + if (!verify_scheduling("main thread", "sandbox_sched_main")) + { + all_pass = false; + } + } + + TEST_STEP("Verify scheduling policy and priority in a spawned thread") + { + // A thread created with default attributes inherits the scheduling policy and + // priority of its creating thread, so the configured real-time settings must + // apply here as well. + std::atomic thread_pass{false}; + std::thread worker( + [&thread_pass]() { thread_pass = verify_scheduling("spawned thread", "sandbox_sched_thread"); }); + worker.join(); + + if (!thread_pass) + { + all_pass = false; + } + } + return all_pass; } From 5af94559df6d1fe1299246c3567281ade968d840 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 14:55:22 +0200 Subject: [PATCH 10/20] Remove not needed debug files --- .../sandbox_options_process.cpp | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index 2da3366de..338f5dd56 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -15,12 +15,11 @@ #include #include #include -#include #include #include #include +#include #include -#include #include #include @@ -51,20 +50,14 @@ const char* policy_name(const int policy) /// @brief Verify that the calling thread runs with the expected scheduling policy and priority. /// @param[in] context Human readable name of the thread, used in failure messages. -/// @param[in] file_prefix Prefix for the result files written for debugging. /// @return true if the policy and priority match the configured values. -bool verify_scheduling(const std::string& context, const std::string& file_prefix) +bool verify_scheduling(const std::string& context) { int policy = -1; sched_param param{}; const int rc = pthread_getschedparam(pthread_self(), &policy, ¶m); EXPECT_EQ(rc, 0) << context << ": pthread_getschedparam failed rc=" << rc; - std::ofstream policy_file{file_prefix + "_policy.txt"}; - policy_file << policy; - std::ofstream prio_file{file_prefix + "_priority.txt"}; - prio_file << param.sched_priority; - bool pass = (rc == 0); EXPECT_EQ(policy, expected_policy) << context << ": Expected scheduling policy=" << policy_name(expected_policy) @@ -98,11 +91,6 @@ bool verify_sandbox_options() const uid_t current_uid = getuid(); const gid_t current_gid = getgid(); - std::ofstream uid_file{"sandbox_uid.txt"}; - uid_file << current_uid; - std::ofstream gid_file{"sandbox_gid.txt"}; - gid_file << current_gid; - EXPECT_EQ(current_uid, expected_uid) << "Expected uid=" << expected_uid << " but got uid=" << current_uid; EXPECT_EQ(current_gid, expected_gid) << "Expected gid=" << expected_gid << " but got gid=" << current_gid; @@ -118,12 +106,6 @@ bool verify_sandbox_options() const int count = getgroups(static_cast(groups.size()), groups.data()); EXPECT_GE(count, 0) << "Failed to get supplementary groups"; - std::ofstream supp_file{"sandbox_supp_groups.txt"}; - for (int i = 0; i < count; ++i) - { - supp_file << groups[i] << (i < count - 1 ? "," : ""); - } - for (const gid_t expected_group : expected_supp_groups) { const bool found = std::find(groups.begin(), groups.begin() + count, expected_group) != groups.end(); @@ -138,15 +120,12 @@ bool verify_sandbox_options() TEST_STEP("Verify working directory") { - char buf[PATH_MAX]; - char* result = getcwd(buf, sizeof(buf)); + std::array buf{}; + char* result = getcwd(buf.data(), buf.size()); EXPECT_NE(result, nullptr) << "Failed to get current working directory"; if (result) { - std::ofstream cwd_file{"sandbox_cwd.txt"}; - cwd_file << result; - EXPECT_EQ(std::string(result), expected_cwd) << "Expected working_dir=" << expected_cwd << " but got cwd=" << result; @@ -159,7 +138,7 @@ bool verify_sandbox_options() TEST_STEP("Verify scheduling policy and priority in the main thread") { - if (!verify_scheduling("main thread", "sandbox_sched_main")) + if (!verify_scheduling("main thread")) { all_pass = false; } @@ -167,12 +146,13 @@ bool verify_sandbox_options() TEST_STEP("Verify scheduling policy and priority in a spawned thread") { - // A thread created with default attributes inherits the scheduling policy and + // A thread created with default attributes inherits the schedulng policy and // priority of its creating thread, so the configured real-time settings must // apply here as well. std::atomic thread_pass{false}; - std::thread worker( - [&thread_pass]() { thread_pass = verify_scheduling("spawned thread", "sandbox_sched_thread"); }); + std::thread worker([&thread_pass]() { + thread_pass = verify_scheduling("spawned thread"); + }); worker.join(); if (!thread_pass) From 5bed47f0468bd36552592763c780bcdb0d57d02e Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 18:51:13 +0200 Subject: [PATCH 11/20] Tiny modifications and comment --- MODULE.bazel | 4 +++- tests/integration/sandbox_options/control_daemon_mock.cpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 22322825f..0bc4bc0bb 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -161,7 +161,9 @@ bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True) # Patch the Docker plugin so tests can request extra container privileges # (e.g. cap_add=["SYS_NICE"], needed for SCHED_FIFO/sched_setscheduler) via the -# `docker_configuration` fixture. Remove once forwarding lands upstream. +# `docker_configuration` fixture. +# FIXME: With next release, this is no longer needed. +# See also https://github.com/eclipse-score/itf/blob/54cab4f6ebc1fb40d0b93a8c1a451a03bda77b6a/score/itf/plugins/docker.py#L381 single_version_override( module_name = "score_itf", patch_strip = 1, diff --git a/tests/integration/sandbox_options/control_daemon_mock.cpp b/tests/integration/sandbox_options/control_daemon_mock.cpp index d0c684303..00a7e9dd8 100644 --- a/tests/integration/sandbox_options/control_daemon_mock.cpp +++ b/tests/integration/sandbox_options/control_daemon_mock.cpp @@ -19,7 +19,7 @@ #include #include -TEST(Smoke, Daemon) +TEST(SandboxOptions, Daemon) { score::mw::lifecycle::ControlClient client{}; ASSERT_TRUE(check_clean({test_end_location})); From e74176a58dfcc3f7466d8601011357276a1e960e Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 19:08:06 +0200 Subject: [PATCH 12/20] Remove double replacement if working_dir is not set Before, when working_dir is not set it has been replaced with binary dir in c++ code. As this workaround is now done in python code, there is no need to keep this in c++ code. --- .../details/process_launcher.cpp | 33 ++++--------------- scripts/config_mapping/lifecycle_config.py | 4 +-- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp index 7cc48c772..1c000b120 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp @@ -22,14 +22,14 @@ #include #include -#include "score/mw/launch_manager/process_group_manager/iprocess.hpp" -#include "score/mw/launch_manager/control/control_client_channel.hpp" #include "score/mw/launch_manager/common/log.hpp" +#include "score/mw/launch_manager/control/control_client_channel.hpp" #include "score/mw/launch_manager/osal/ipc_comms.hpp" #include "score/mw/launch_manager/osal/security_policy.hpp" #include "score/mw/launch_manager/osal/set_affinity.hpp" #include "score/mw/launch_manager/osal/set_groups.hpp" #include "score/mw/launch_manager/osal/sys_exit.hpp" +#include "score/mw/launch_manager/process_group_manager/iprocess.hpp" #include #include #include @@ -133,32 +133,11 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) void changeCurrentWorkingDirectory(const score::lcm::internal::osal::OsalConfig& config) { - if (!config.working_dir_.empty()) - { - if (-1 == chdir(config.working_dir_.c_str())) - { - LM_LOG_ERROR() << "[New process] chdir(" << config.working_dir_ << ") failed:" << score::lcm::internal::errno_message(errno); - sysexit(EXIT_FAILURE); - } - return; - } - - // Change current working directory to the same as the executable - constexpr size_t string_size = static_cast(PATH_MAX); - // Notice that this next static variable is duplicated by the fork() and so does not need - // any protection by a mutex although at first sight you may think it could need one. - static char path_copy[string_size + 1U] = {0}; - - if (config.executable_path_.size() >= string_size) - { - LM_LOG_ERROR() << "[New process] executable path longer than" << string_size - << "chars:" << config.executable_path_; - sysexit(EXIT_FAILURE); - } - - if (-1 == chdir(dirname(strncpy(path_copy, config.executable_path_.c_str(), string_size)))) + // working_dir_ is set by python configuration generator in lifecycle_config.py, so it should always be valid. + // If not, chdir will fail anyway and we will log an error and exit. + if (-1 == chdir(config.working_dir_.c_str())) { - LM_LOG_ERROR() << "[New process] chdir(" << config.executable_path_ + LM_LOG_ERROR() << "[New process] chdir(" << config.working_dir_ << ") failed:" << score::lcm::internal::errno_message(errno); sysexit(EXIT_FAILURE); } diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 49b467655..e0ffec16b 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -293,9 +293,7 @@ def gen_config(output_dir, config, input_filename, schema_version=None): "shutdown_timeout": depl_cfg["shutdown_timeout"], "bin_dir": depl_cfg["bin_dir"], # Default the working directory to bin_dir (the directory the - # executable lives in) when not set explicitly. This matches the - # launch manager's own fallback of chdir'ing to the executable's - # directory when no working_dir is configured. + # executable lives in) when not set explicitly. "working_dir": depl_cfg.get("working_dir", depl_cfg["bin_dir"]), "sandbox": sandbox_out, } From 04fe95ed5e97c543611d70e1450c4d96ebc7c6d2 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 08:45:28 +0200 Subject: [PATCH 13/20] Fix formatting --- MODULE.bazel | 2 +- scripts/config_mapping/lifecycle_config.py | 3 +-- tests/integration/sandbox_options/sandbox_options.py | 4 ++-- tests/utils/testing_utils/test_results.py | 4 +++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 0bc4bc0bb..0d6d5a2b7 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -161,7 +161,7 @@ bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True) # Patch the Docker plugin so tests can request extra container privileges # (e.g. cap_add=["SYS_NICE"], needed for SCHED_FIFO/sched_setscheduler) via the -# `docker_configuration` fixture. +# `docker_configuration` fixture. # FIXME: With next release, this is no longer needed. # See also https://github.com/eclipse-score/itf/blob/54cab4f6ebc1fb40d0b93a8c1a451a03bda77b6a/score/itf/plugins/docker.py#L381 single_version_override( diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index e0ffec16b..cf1fc8648 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -674,8 +674,7 @@ def get_terminating_behavior(component_config): "ready_recovery_action" ]["restart"]["number_of_attempts"] process["working_dir"] = component_config["deployment_config"].get( - "working_dir", - component_config["deployment_config"]["bin_dir"] + "working_dir", component_config["deployment_config"]["bin_dir"] ) match component_config["component_properties"]["application_profile"][ diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 1019f8f81..068e0f5a6 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -33,7 +33,7 @@ def docker_configuration(): "feat_req__lifecycle__launch_priority_support", "feat_req__lifecycle__scheduling_policy", "feat_req__lifecycle__cwd_support", - "feat_req__lifecycle__supplementary_groups" + "feat_req__lifecycle__supplementary_groups", ], partially_verifies=[], test_type="interface-test", @@ -42,7 +42,7 @@ def docker_configuration(): def test_sandbox_options(target, setup_test, assert_test_results, remote_test_dir): """ Objective: Verifies the effectiveness of sandbox-options as gid, uid, supplementary groups, and scheduling policy. - + The launch manager starts with an initial run target. The control daemon activates the "Running" run target (starting the managed process with the sandbox options applied), then transitions back to "Startup", and finally activates "Off". Expected Behaviour: All run target transitions complete successfully and all processes report running. """ diff --git a/tests/utils/testing_utils/test_results.py b/tests/utils/testing_utils/test_results.py index 15e097a97..083a7aea0 100644 --- a/tests/utils/testing_utils/test_results.py +++ b/tests/utils/testing_utils/test_results.py @@ -51,6 +51,7 @@ def download_xml_results( except Exception: pass + def get_failing_files(path: Path): """Collects all produced xml files and returns that set as well as the subset of failing files""" failing_files = set() @@ -83,7 +84,8 @@ def test_foo(assert_test_results, ...): """ def _assert( - expected_xml_files: Set[str], additional_search_dirs: Iterable[Union[Path, str]] = () + expected_xml_files: Set[str], + additional_search_dirs: Iterable[Union[Path, str]] = (), ): # Show the error as coming from the call in the test rather than here __tracebackhide__ = True From 71264de7672daa82f30e4368a6d5ccc5b3bc7c87 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 11:44:40 +0200 Subject: [PATCH 14/20] Revert no longer needed patch for score_itf With new version 0.5.0 this is no longer needed. --- MODULE.bazel | 12 ------------ patches/score_itf_docker_cap_add.patch | 11 ----------- 2 files changed, 23 deletions(-) delete mode 100644 patches/score_itf_docker_cap_add.patch diff --git a/MODULE.bazel b/MODULE.bazel index 0d6d5a2b7..adf6115c6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -159,18 +159,6 @@ use_repo(pip, "score_lifecycle_pip") bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True) -# Patch the Docker plugin so tests can request extra container privileges -# (e.g. cap_add=["SYS_NICE"], needed for SCHED_FIFO/sched_setscheduler) via the -# `docker_configuration` fixture. -# FIXME: With next release, this is no longer needed. -# See also https://github.com/eclipse-score/itf/blob/54cab4f6ebc1fb40d0b93a8c1a451a03bda77b6a/score/itf/plugins/docker.py#L381 -single_version_override( - module_name = "score_itf", - patch_strip = 1, - patches = ["//patches:score_itf_docker_cap_add.patch"], - version = "0.3.0", -) - oci = use_extension("@rules_oci//oci:extensions.bzl", "oci", dev_dependency = True) oci.pull( name = "debian-test-runtime", diff --git a/patches/score_itf_docker_cap_add.patch b/patches/score_itf_docker_cap_add.patch deleted file mode 100644 index f2a372799..000000000 --- a/patches/score_itf_docker_cap_add.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- a/score/itf/plugins/docker.py -+++ b/score/itf/plugins/docker.py -@@ -331,6 +331,8 @@ def target_init(request, _docker_configuration): - environment=_docker_configuration["environment"], - volumes=_docker_configuration["volumes"], - shm_size=_docker_configuration["shm_size"], -+ privileged=_docker_configuration.get("privileged", False), -+ cap_add=_docker_configuration.get("cap_add"), - ) - target = None - try: From ba851bb773fe0936be8c71f301ad065a5bc6880a Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 14:53:33 +0200 Subject: [PATCH 15/20] Test different variations of working_dir presence Two test modifications: - If no working_dir is defined, bin directory is used. - A working dir defined on deployment config level can be overridden on component level --- .../tests/basic_test/expected_output/lm_demo.json | 10 +++++----- .../tests/basic_test/input/lm_config.json | 1 - .../tests/lm_config_test/expected_output/lm_demo.json | 2 +- .../tests/lm_config_test/input/lm_config.json | 3 ++- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json index ea0e5c0e5..1878015f6 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json @@ -20,7 +20,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/scripts", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -85,7 +85,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/apps/dlt-daemon", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -154,7 +154,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/apps/someip", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -222,7 +222,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/apps/test_app1", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -299,7 +299,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/apps/state_manager", "executable_reporting_behavior": "ReportsExecutionState", "function_cluster_affiliation": "STATE_MANAGEMENT", "startup_config": [ diff --git a/scripts/config_mapping/tests/basic_test/input/lm_config.json b/scripts/config_mapping/tests/basic_test/input/lm_config.json index b35597f9e..ab8402597 100644 --- a/scripts/config_mapping/tests/basic_test/input/lm_config.json +++ b/scripts/config_mapping/tests/basic_test/input/lm_config.json @@ -10,7 +10,6 @@ "EMPTY_GLOBAL_ENV_VAR": "" }, "bin_dir": "/opt", - "working_dir": "/tmp", "ready_recovery_action": { "restart": { "number_of_attempts": 1, diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json index 8b8e5a5fb..9966f9104 100644 --- a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json @@ -17,7 +17,7 @@ ], "security_policy_details": "policy_name_2", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/apps/test_app2", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { diff --git a/scripts/config_mapping/tests/lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json index f825876f8..90007280a 100644 --- a/scripts/config_mapping/tests/lm_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json @@ -85,7 +85,8 @@ "scheduling_priority": 99, "max_memory_usage": 2048, "max_cpu_usage": 50 - } + }, + "working_dir": "/opt/apps/test_app2" } }, "setup_filesystem_sh": { From 48a1f65db63083e1981cb4074dfa489c75994935 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 15:37:52 +0200 Subject: [PATCH 16/20] Parameterize sandbox_options_process executable --- .../sandbox_options/sandbox_options.json | 10 +- .../sandbox_options_process.cpp | 157 +++++++++++++++--- 2 files changed, 145 insertions(+), 22 deletions(-) diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index 38dcfeac3..dc35e08d8 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -65,7 +65,15 @@ "binary_name": "sandbox_options_process", "application_profile": { "application_type": "Reporting" - } + }, + "process_arguments": [ + "--uid=666", + "--gid=666", + "--supplementary-groups=123,321,456", + "--scheduling-policy=SCHED_FIFO", + "--scheduling-priority=10", + "--working-dir=/tmp" + ] }, "deployment_config": { "environmental_variables": { diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index 338f5dd56..dfa3081a1 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -20,8 +20,12 @@ #include #include #include +#include +#include #include +#include #include +#include #include "tests/utils/test_helper/test_helper.hpp" #include @@ -29,9 +33,23 @@ namespace { -// Values configured for sandbox_options_process in sandbox_options.json. -constexpr int expected_policy = SCHED_FIFO; -constexpr int expected_priority = 10; +/// @brief Expected sandbox option values, supplied to this process on the command line. +/// +/// The launch manager applies the sandbox options from sandbox_options.json and passes the same +/// values to the managed process via 'process_arguments', so the test verifies the applied state +/// against the configured expectation without duplicating any literals here. +struct ExpectedValues +{ + int policy = SCHED_OTHER; + int priority = 0; + uid_t uid = 0; + gid_t gid = 0; + std::vector supplementary_groups{}; + std::string working_dir{}; +}; + +// Populated from argv in main() before the tests run. +ExpectedValues expected; const char* policy_name(const int policy) { @@ -48,6 +66,101 @@ const char* policy_name(const int policy) } } +int parse_policy(const std::string& name) +{ + if (name == "SCHED_FIFO") + { + return SCHED_FIFO; + } + if (name == "SCHED_RR") + { + return SCHED_RR; + } + if (name == "SCHED_OTHER") + { + return SCHED_OTHER; + } + return -1; +} + +// Parse a comma separated list of group ids, e.g. "123,321,456". +std::vector parse_groups(const std::string& csv) +{ + std::vector groups; + std::stringstream stream(csv); + std::string token; + while (std::getline(stream, token, ',')) + { + if (!token.empty()) + { + groups.push_back(static_cast(std::stoul(token))); + } + } + return groups; +} + +// If 'arg' has the form "--=", store the value in 'value' and return true. +bool match_option(const std::string& arg, const std::string_view key, std::string& value) +{ + const std::string prefix = "--" + std::string(key) + "="; + if (arg.rfind(prefix, 0) == 0) + { + value = arg.substr(prefix.size()); + return true; + } + return false; +} + +// Populate 'expected' from the command line arguments. Returns true if every expected value was +// provided. +bool parse_arguments(int argc, char** argv, ExpectedValues& out) +{ + bool has_policy = false; + bool has_priority = false; + bool has_uid = false; + bool has_gid = false; + bool has_groups = false; + bool has_working_dir = false; + + std::string value; + for (int i = 1; i < argc; ++i) + { + const std::string arg = argv[i]; + if (match_option(arg, "uid", value)) + { + out.uid = static_cast(std::stoul(value)); + has_uid = true; + } + else if (match_option(arg, "gid", value)) + { + out.gid = static_cast(std::stoul(value)); + has_gid = true; + } + else if (match_option(arg, "supplementary-groups", value)) + { + out.supplementary_groups = parse_groups(value); + has_groups = true; + } + else if (match_option(arg, "scheduling-policy", value)) + { + out.policy = parse_policy(value); + has_policy = true; + } + else if (match_option(arg, "scheduling-priority", value)) + { + out.priority = std::stoi(value); + has_priority = true; + } + else if (match_option(arg, "working-dir", value)) + { + out.working_dir = value; + has_working_dir = true; + } + } + + return has_policy && has_priority && has_uid && has_gid && has_groups && has_working_dir; +} + /// @brief Verify that the calling thread runs with the expected scheduling policy and priority. /// @param[in] context Human readable name of the thread, used in failure messages. /// @return true if the policy and priority match the configured values. @@ -60,16 +173,16 @@ bool verify_scheduling(const std::string& context) bool pass = (rc == 0); - EXPECT_EQ(policy, expected_policy) << context << ": Expected scheduling policy=" << policy_name(expected_policy) + EXPECT_EQ(policy, expected.policy) << context << ": Expected scheduling policy=" << policy_name(expected.policy) << " but got " << policy_name(policy); - if (policy != expected_policy) + if (policy != expected.policy) { pass = false; } - EXPECT_EQ(param.sched_priority, expected_priority) - << context << ": Expected scheduling priority=" << expected_priority << " but got " << param.sched_priority; - if (param.sched_priority != expected_priority) + EXPECT_EQ(param.sched_priority, expected.priority) + << context << ": Expected scheduling priority=" << expected.priority << " but got " << param.sched_priority; + if (param.sched_priority != expected.priority) { pass = false; } @@ -81,20 +194,15 @@ bool verify_sandbox_options() { bool all_pass = true; - const uid_t expected_uid = 666; - const gid_t expected_gid = 666; - const std::array expected_supp_groups = {123, 321, 456}; - const std::string expected_cwd = "/tmp"; - TEST_STEP("Verify uid and gid") { const uid_t current_uid = getuid(); const gid_t current_gid = getgid(); - EXPECT_EQ(current_uid, expected_uid) << "Expected uid=" << expected_uid << " but got uid=" << current_uid; - EXPECT_EQ(current_gid, expected_gid) << "Expected gid=" << expected_gid << " but got gid=" << current_gid; + EXPECT_EQ(current_uid, expected.uid) << "Expected uid=" << expected.uid << " but got uid=" << current_uid; + EXPECT_EQ(current_gid, expected.gid) << "Expected gid=" << expected.gid << " but got gid=" << current_gid; - if (current_uid != expected_uid || current_gid != expected_gid) + if (current_uid != expected.uid || current_gid != expected.gid) { all_pass = false; } @@ -106,7 +214,7 @@ bool verify_sandbox_options() const int count = getgroups(static_cast(groups.size()), groups.data()); EXPECT_GE(count, 0) << "Failed to get supplementary groups"; - for (const gid_t expected_group : expected_supp_groups) + for (const gid_t expected_group : expected.supplementary_groups) { const bool found = std::find(groups.begin(), groups.begin() + count, expected_group) != groups.end(); EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" << count @@ -126,10 +234,10 @@ bool verify_sandbox_options() if (result) { - EXPECT_EQ(std::string(result), expected_cwd) - << "Expected working_dir=" << expected_cwd << " but got cwd=" << result; + EXPECT_EQ(std::string(result), expected.working_dir) + << "Expected working_dir=" << expected.working_dir << " but got cwd=" << result; - if (std::string(result) != expected_cwd) + if (std::string(result) != expected.working_dir) { all_pass = false; } @@ -172,7 +280,14 @@ TEST(SandboxOptions, RunAndVerify) score::mw::lifecycle::report_running(); } -int main() +int main(int argc, char** argv) { + if (!parse_arguments(argc, argv, expected)) + { + std::cerr << "Missing expected sandbox option(s) on the command line. Required: --uid, --gid, " + "--supplementary-groups, --scheduling-policy, --scheduling-priority, --working-dir" + << std::endl; + return 1; + } return TestRunner(__FILE__).RunTests(); } \ No newline at end of file From c48cf0b3df6aea64ce091685bff2e03b6306ca7b Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 15:42:49 +0200 Subject: [PATCH 17/20] Remove exports_files, which has been added by fault --- tests/integration/sandbox_options/BUILD | 5 ----- tests/integration/smoke/BUILD | 5 ----- 2 files changed, 10 deletions(-) diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 08bba667a..2857a0fed 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -13,11 +13,6 @@ load("@rules_cc//cc:defs.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") -exports_files( - ["lm_demo.json"], - visibility = ["//examples:__subpackages__"], -) - cc_binary( name = "control_daemon_mock", srcs = ["control_daemon_mock.cpp"], diff --git a/tests/integration/smoke/BUILD b/tests/integration/smoke/BUILD index 506497c00..127c143a4 100644 --- a/tests/integration/smoke/BUILD +++ b/tests/integration/smoke/BUILD @@ -13,11 +13,6 @@ load("@rules_cc//cc:defs.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") -exports_files( - ["lm_demo.json"], - visibility = ["//examples:__subpackages__"], -) - cc_binary( name = "control_daemon_mock", srcs = ["control_daemon_mock.cpp"], From 0af6e7759cf702d194bf26ac8168f3c31c480f7e Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 16:23:33 +0200 Subject: [PATCH 18/20] kill with SIGKILL if process does not react on SIGTERM --- .../testing_utils/run_until_file_deployed.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/utils/testing_utils/run_until_file_deployed.py b/tests/utils/testing_utils/run_until_file_deployed.py index 5eda22027..8c23111ca 100644 --- a/tests/utils/testing_utils/run_until_file_deployed.py +++ b/tests/utils/testing_utils/run_until_file_deployed.py @@ -35,7 +35,7 @@ def run_until_file_deployed( :param binary_path: path to the binary to execute on the target. :param file_path: path of the file to wait for on the target. :param timeout_s: maximum seconds to wait for the file (default: 30). - :param poll_interval_s: seconds between file-existence checks (default: 0.5). + :param poll_interval_s: seconds between file and process checks (default: 0.5). :param stop_timeout_s: maximum seconds to wait for the process to terminate after SIGTERM (default: 2). :param args: optional list of arguments to pass to the binary. @@ -67,7 +67,7 @@ def run_until_file_deployed( # run their cleanup code before exiting. kill_cmd = f"kill -15 -{proc.pid()}" res, _ = target.execute(kill_cmd) - assert res == 0, "Couldn't kill lcm" + assert res == 0, "Couldn't kill process with SIGTERM" # Poll for the process to actually terminate instead of assuming a # single fixed grace period is enough. Termination timing varies @@ -76,11 +76,17 @@ def run_until_file_deployed( while proc.is_running() and time.monotonic() < stop_deadline: time.sleep(poll_interval_s) - assert not proc.is_running(), ( - f"LCM still running {stop_timeout_s}s after SIGTERM" - ) + if proc.is_running(): + kill_cmd = f"kill -9 -{proc.pid()}" + res, _ = target.execute(kill_cmd) + assert res == 0, "Couldn't kill process with SIGKILL" + # Let this test fail so we can see the logs and figure out why the process didn't exit cleanly. + assert False, ( + f"Process still running {stop_timeout_s}s after SIGTERM" + ) + assert proc.get_exit_code() == 0, ( - f"LCM did not exit cleanly, it died with code {proc.get_exit_code()}" + f"Process did not exit cleanly, it died with code {proc.get_exit_code()}" ) return proc logger.debug(f"Waiting for {file_path}") From fc60ca68b069ed8d7b07804480c6d384897f7071 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 27 Jul 2026 12:41:04 +0200 Subject: [PATCH 19/20] Test more scheduling params - add another process - parameterize sandbox_options_process.cpp --- tests/integration/sandbox_options/BUILD | 19 +- .../sandbox_options/sandbox_options.json | 32 ++- .../sandbox_options/sandbox_options.py | 13 +- .../sandbox_options_process.cpp | 213 +++++++++++------- 4 files changed, 186 insertions(+), 91 deletions(-) diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 2857a0fed..018c71319 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -25,8 +25,22 @@ cc_binary( ], ) +# Two binaries are built from the same source. Each verifies the sandbox options passed to it on +# the command line (see sandbox_options.json), and derives its GTest XML result file name from its +# own executable name so the results do not collide. cc_binary( - name = "sandbox_options_process", + name = "sandbox_options_process_a", + srcs = ["sandbox_options_process.cpp"], + deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "sandbox_options_process_b", srcs = ["sandbox_options_process.cpp"], deps = [ "//score/launch_manager:control_cc", @@ -41,7 +55,8 @@ integration_test( srcs = ["sandbox_options.py"], binaries = [ ":control_daemon_mock", - ":sandbox_options_process", + ":sandbox_options_process_a", + ":sandbox_options_process_b", "//score/launch_manager", ], config = ":sandbox_options.json", diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index dc35e08d8..5b22e9127 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -60,9 +60,9 @@ } } }, - "sandbox_options_process": { + "sandbox_options_process_a": { "component_properties": { - "binary_name": "sandbox_options_process", + "binary_name": "sandbox_options_process_a", "application_profile": { "application_type": "Reporting" }, @@ -88,6 +88,31 @@ }, "working_dir": "/tmp" } + }, + "sandbox_options_process_b": { + "component_properties": { + "binary_name": "sandbox_options_process_b", + "application_profile": { + "application_type": "Reporting" + }, + "process_arguments": [ + "--uid=0", + "--gid=0", + "--supplementary-groups=", + "--scheduling-policy=SCHED_RR" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "DefaultPG_app1" + }, + "sandbox": { + "uid": 0, + "gid": 0, + "supplementary_group_ids": [], + "scheduling_policy": "SCHED_RR" + } + } } }, "run_targets": { @@ -104,7 +129,8 @@ "Running": { "depends_on": [ "control_daemon", - "sandbox_options_process" + "sandbox_options_process_a", + "sandbox_options_process_b" ], "recovery_action": { "switch_run_target": { diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 068e0f5a6..2a00c7bf6 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -61,10 +61,15 @@ def test_sandbox_options(target, setup_test, assert_test_results, remote_test_di timeout_s=3.0, ) - # The managed process is configured with a custom working directory (see the - # "working_dir" entries in sandbox_options.json), so its XML result file is - # written there rather than into remote_test_dir. Search those directories too. + # sandbox_options_process_a is configured with a custom working directory ("working_dir": "/tmp" + # in sandbox_options.json), so its XML result file is written there rather than into + # remote_test_dir. sandbox_options_process_b has no working directory configured and therefore + # writes into remote_test_dir. Search all relevant directories. assert_test_results( - {"control_daemon_mock.xml", "sandbox_options_process.xml"}, + { + "control_daemon_mock.xml", + "sandbox_options_process_a.xml", + "sandbox_options_process_b.xml", + }, additional_search_dirs=["/tmp/tests/sandbox_options", "/tmp"], ) diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index dfa3081a1..bae967e7c 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -38,14 +39,17 @@ namespace /// The launch manager applies the sandbox options from sandbox_options.json and passes the same /// values to the managed process via 'process_arguments', so the test verifies the applied state /// against the configured expectation without duplicating any literals here. +/// +/// Every value is optional: a value that is not passed on the command line is not verified. This +/// lets a component leave an option unset (e.g. no working directory) without the test flagging it. struct ExpectedValues { - int policy = SCHED_OTHER; - int priority = 0; - uid_t uid = 0; - gid_t gid = 0; - std::vector supplementary_groups{}; - std::string working_dir{}; + std::optional policy; + std::optional priority; + std::optional uid; + std::optional gid; + std::optional> supplementary_groups; + std::optional working_dir; }; // Populated from argv in main() before the tests run. @@ -111,16 +115,12 @@ bool match_option(const std::string& arg, const std::string_view key, std::strin return false; } -// Populate 'expected' from the command line arguments. Returns true if every expected value was -// provided. +// Populate 'out' from the command line arguments. Only the options that are actually present are +// set; any option left out stays unset and is therefore not verified. Returns false if an +// unrecognized option is encountered. bool parse_arguments(int argc, char** argv, ExpectedValues& out) { - bool has_policy = false; - bool has_priority = false; - bool has_uid = false; - bool has_gid = false; - bool has_groups = false; - bool has_working_dir = false; + bool all_recognized = true; std::string value; for (int i = 1; i < argc; ++i) @@ -129,60 +129,77 @@ bool parse_arguments(int argc, char** argv, ExpectedValues& out) if (match_option(arg, "uid", value)) { out.uid = static_cast(std::stoul(value)); - has_uid = true; } else if (match_option(arg, "gid", value)) { out.gid = static_cast(std::stoul(value)); - has_gid = true; } else if (match_option(arg, "supplementary-groups", value)) { out.supplementary_groups = parse_groups(value); - has_groups = true; } else if (match_option(arg, "scheduling-policy", value)) { out.policy = parse_policy(value); - has_policy = true; } else if (match_option(arg, "scheduling-priority", value)) { out.priority = std::stoi(value); - has_priority = true; } else if (match_option(arg, "working-dir", value)) { out.working_dir = value; - has_working_dir = true; + } + else + { + std::cerr << "Unrecognized argument: " << arg << std::endl; + all_recognized = false; } } - return has_policy && has_priority && has_uid && has_gid && has_groups && has_working_dir; + return all_recognized; } /// @brief Verify that the calling thread runs with the expected scheduling policy and priority. /// @param[in] context Human readable name of the thread, used in failure messages. -/// @return true if the policy and priority match the configured values. +/// @return true if the policy (when provided) and the priority match. bool verify_scheduling(const std::string& context) { int policy = -1; sched_param param{}; - const int rc = pthread_getschedparam(pthread_self(), &policy, ¶m); - EXPECT_EQ(rc, 0) << context << ": pthread_getschedparam failed rc=" << rc; + const int result = pthread_getschedparam(pthread_self(), &policy, ¶m); + EXPECT_EQ(result, 0) << context << ": pthread_getschedparam failed rc=" << result; + if (result != 0) + { + // 'policy' was not written, so it is still the sentinel -1. Bail out before it can reach + // sched_get_priority_min() below (which would return -1 and set errno=EINVAL). + return false; + } - bool pass = (rc == 0); + bool pass = true; - EXPECT_EQ(policy, expected.policy) << context << ": Expected scheduling policy=" << policy_name(expected.policy) - << " but got " << policy_name(policy); - if (policy != expected.policy) + if (expected.policy.has_value()) { - pass = false; + EXPECT_EQ(policy, *expected.policy) + << context << ": Expected scheduling policy=" << policy_name(*expected.policy) << " but got " + << policy_name(policy); + if (policy != *expected.policy) + { + pass = false; + } } - EXPECT_EQ(param.sched_priority, expected.priority) - << context << ": Expected scheduling priority=" << expected.priority << " but got " << param.sched_priority; - if (param.sched_priority != expected.priority) + // The priority is always verified. When no explicit priority is provided on the command line, + // verify against the default the launch manager ends up applying: its configured default (0) + // clamped up to the policy minimum, i.e. sched_get_priority_min() for the effective policy + // (1 for SCHED_FIFO/SCHED_RR, 0 for SCHED_OTHER). 'policy' is the value retrieved above and is + // guaranteed valid here (the rc != 0 case returned early). + const int effective_policy = expected.policy.value_or(policy); + const int expected_priority = expected.priority.value_or(sched_get_priority_min(effective_policy)); + EXPECT_EQ(param.sched_priority, expected_priority) + << context << ": Expected scheduling priority=" << expected_priority + << (expected.priority.has_value() ? "" : " (default)") << " but got " << param.sched_priority; + if (param.sched_priority != expected_priority) { pass = false; } @@ -194,78 +211,105 @@ bool verify_sandbox_options() { bool all_pass = true; - TEST_STEP("Verify uid and gid") + if (expected.uid.has_value() || expected.gid.has_value()) { - const uid_t current_uid = getuid(); - const gid_t current_gid = getgid(); - - EXPECT_EQ(current_uid, expected.uid) << "Expected uid=" << expected.uid << " but got uid=" << current_uid; - EXPECT_EQ(current_gid, expected.gid) << "Expected gid=" << expected.gid << " but got gid=" << current_gid; - - if (current_uid != expected.uid || current_gid != expected.gid) + TEST_STEP("Verify uid and gid") { - all_pass = false; + if (expected.uid.has_value()) + { + const uid_t current_uid = getuid(); + EXPECT_EQ(current_uid, *expected.uid) + << "Expected uid=" << *expected.uid << " but got uid=" << current_uid; + if (current_uid != *expected.uid) + { + all_pass = false; + } + } + + if (expected.gid.has_value()) + { + const gid_t current_gid = getgid(); + EXPECT_EQ(current_gid, *expected.gid) + << "Expected gid=" << *expected.gid << " but got gid=" << current_gid; + if (current_gid != *expected.gid) + { + all_pass = false; + } + } } } - TEST_STEP("Verify supplementary groups") + if (expected.supplementary_groups.has_value()) { - std::vector groups(256); - const int count = getgroups(static_cast(groups.size()), groups.data()); - EXPECT_GE(count, 0) << "Failed to get supplementary groups"; - - for (const gid_t expected_group : expected.supplementary_groups) + TEST_STEP("Verify supplementary groups") { - const bool found = std::find(groups.begin(), groups.begin() + count, expected_group) != groups.end(); - EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" << count - << " total)]"; - if (!found) + std::vector groups(256); + const int count = getgroups(static_cast(groups.size()), groups.data()); + EXPECT_GE(count, 0) << "Failed to get supplementary groups"; + if (count >= 0) { - all_pass = false; + groups.resize(static_cast(count)); + } + + for (const gid_t expected_group : *expected.supplementary_groups) + { + const bool found = std::find(groups.begin(), groups.end(), expected_group) != groups.end(); + EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" + << count << " total)]"; + if (!found) + { + all_pass = false; + } } } } - TEST_STEP("Verify working directory") + if (expected.working_dir.has_value()) { - std::array buf{}; - char* result = getcwd(buf.data(), buf.size()); - EXPECT_NE(result, nullptr) << "Failed to get current working directory"; - - if (result) + TEST_STEP("Verify working directory") { - EXPECT_EQ(std::string(result), expected.working_dir) - << "Expected working_dir=" << expected.working_dir << " but got cwd=" << result; + std::array buf{}; + char* result = getcwd(buf.data(), buf.size()); + EXPECT_NE(result, nullptr) << "Failed to get current working directory"; - if (std::string(result) != expected.working_dir) + if (result) { - all_pass = false; + EXPECT_EQ(std::string(result), *expected.working_dir) + << "Expected working_dir=" << *expected.working_dir << " but got cwd=" << result; + + if (std::string(result) != *expected.working_dir) + { + all_pass = false; + } } } } - TEST_STEP("Verify scheduling policy and priority in the main thread") + if (expected.policy.has_value() || expected.priority.has_value()) { - if (!verify_scheduling("main thread")) + TEST_STEP("Verify scheduling policy and priority in the main thread") { - all_pass = false; + if (!verify_scheduling("main thread")) + { + all_pass = false; + } } - } - TEST_STEP("Verify scheduling policy and priority in a spawned thread") - { - // A thread created with default attributes inherits the schedulng policy and - // priority of its creating thread, so the configured real-time settings must - // apply here as well. - std::atomic thread_pass{false}; - std::thread worker([&thread_pass]() { - thread_pass = verify_scheduling("spawned thread"); - }); - worker.join(); - - if (!thread_pass) + TEST_STEP("Verify scheduling policy and priority in a spawned thread") { - all_pass = false; + // A thread created with default attributes inherits the schedulng policy and + // priority of its creating thread, so the configured real-time settings must + // apply here as well. + std::atomic thread_pass{false}; + std::thread worker([&thread_pass]() { + thread_pass = verify_scheduling("spawned thread"); + }); + worker.join(); + + if (!thread_pass) + { + all_pass = false; + } } } @@ -284,10 +328,15 @@ int main(int argc, char** argv) { if (!parse_arguments(argc, argv, expected)) { - std::cerr << "Missing expected sandbox option(s) on the command line. Required: --uid, --gid, " - "--supplementary-groups, --scheduling-policy, --scheduling-priority, --working-dir" + std::cerr << "Recognized sandbox options: --uid, --gid, --supplementary-groups, " + "--scheduling-policy, --scheduling-priority, --working-dir" << std::endl; return 1; } - return TestRunner(__FILE__).RunTests(); + // Derive the GTest XML result file name from the executable name (argv[0]) rather than the + // shared source file, so that the two binaries built from this source (sandbox_options_process_a + // and sandbox_options_process_b) write distinct result files. Dereference rather than index to + // avoid the no-pointer-arithmetic lint rule. + const char* const executable_path = (argc > 0) ? *argv : __FILE__; + return TestRunner(executable_path).RunTests(); } \ No newline at end of file From f49fd5f13d53eb6f1160cc91b64616a4f93cc7b4 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 27 Jul 2026 13:23:33 +0200 Subject: [PATCH 20/20] Introduce get_working_dir --- scripts/config_mapping/lifecycle_config.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index cf1fc8648..a5f9deeab 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -107,6 +107,14 @@ def get_recovery_process_group_state(config): def sec_to_ms(sec: float) -> int: return int(sec * 1000) +def get_working_dir(deployment_config): + """ + Get the working directory for a component. If not specified, default to the bin_dir. + """ + return deployment_config.get( + "working_dir", deployment_config["bin_dir"] + ) + def preprocess_defaults(global_defaults, config): """ @@ -294,7 +302,7 @@ def gen_config(output_dir, config, input_filename, schema_version=None): "bin_dir": depl_cfg["bin_dir"], # Default the working directory to bin_dir (the directory the # executable lives in) when not set explicitly. - "working_dir": depl_cfg.get("working_dir", depl_cfg["bin_dir"]), + "working_dir": get_working_dir(depl_cfg), "sandbox": sandbox_out, } @@ -673,9 +681,7 @@ def get_terminating_behavior(component_config): process["number_of_restart_attempts"] = component_config["deployment_config"][ "ready_recovery_action" ]["restart"]["number_of_attempts"] - process["working_dir"] = component_config["deployment_config"].get( - "working_dir", component_config["deployment_config"]["bin_dir"] - ) + process["working_dir"] = get_working_dir(component_config["deployment_config"]) match component_config["component_properties"]["application_profile"][ "application_type"