diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index 418af1441..6228a54bf 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -51,15 +51,23 @@ ProcessInfoNode::ProcessInfoNode( IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecycle::ProcessState new_state) { + if (new_state == ProcessState::kFailed) + { + // Didn't reach running or startup + return tryReportError(ComponentError::kErrorBeforeReady); + } + ProcessState desired_state{}; + bool has_process_state_condition = false; const auto& ready_condition = config_.component_properties.ready_condition; std::visit( - [&desired_state](auto&& arg) { + [&desired_state, &has_process_state_condition](auto&& arg) { using ReadyCondT = std::decay_t; if constexpr (std::is_same_v) { + has_process_state_condition = true; switch (arg) { case configuration::ProcessState::Running: @@ -73,12 +81,9 @@ IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecy }, ready_condition); - if (new_state == ProcessState::kFailed) - { - // Didn't reach running or startup - return tryReportError(ComponentError::kErrorBeforeReady); - } - if (new_state == desired_state) + // Reaching the desired state or beyond satisfies the ready condition: a self-terminating process + // may already have exited (kTerminated) by the time completion is reported. + if (has_process_state_condition && new_state >= desired_state) { return tryReportSuccess(); } @@ -274,7 +279,12 @@ IComponent::RequestResult ProcessInfoNode::startProcess(score::cpp::stop_token s } setState(ProcessState::kRunning); // Can fail if we've terminated already - return tryReportCompletion(ProcessState::kRunning); + + // A self-terminating process may already have exited before startup completed. tryHandleTermination() + // leaves such a node waiting for the startup thread, so report against the state actually reached. + const ProcessState reached_state = + (getState() == ProcessState::kTerminated) ? ProcessState::kTerminated : ProcessState::kRunning; + return tryReportCompletion(reached_state); } void ProcessInfoNode::setupControlClientChannel() diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp index 6c176d40a..08d487aaf 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp @@ -243,6 +243,32 @@ TEST_F(ProcessInfoNodeStartupTest, SelfTerminating_ExitsBeforeMapInsert_ReturnsS ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kTerminated)); } +TEST_F(ProcessInfoNodeStartupTest, SelfTerminating_TerminatedReadyCondition_ExitsBeforeMapInsert_ReturnsSuccess) +{ + RecordProperty( + "Description", + "A self-terminating process whose ready condition is Terminated and that exits with status 0 before the map " + "insertion completes reports success from activate() instead of waiting forever."); + + auto node = createProcessInfoNode( + configuration::ApplicationType::Native, 0U, true, configuration::ProcessState::Terminated); + // Simulate the process exiting before the map insertion happens. + EXPECT_CALL(mock_processIf_, startProcess(_, _, _)) + .WillOnce(DoAll( + InvokeWithoutArgs([node = node.get()] { + static_cast(node->tryHandleTermination(0)); + }), + Return(osal::OsalReturnType::kSuccess))); + EXPECT_CALL(*process_map_, insertIfNotTerminated(_, _)) + .WillOnce(Return(score::mw::lifecycle::internal::SafeProcessMapReturnType::kYield)); + + auto result = node->activate(score::cpp::stop_token{}); + + ASSERT_THAT(result.has_value(), IsTrue()); + ASSERT_THAT(result.value(), Eq(IComponent::RequestState::kSuccess)); + ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kTerminated)); +} + TEST_F(ProcessInfoNodeStartupTest, ActivateAlreadyActiveNode_ReturnsSuccess) { RecordProperty( diff --git a/tests/integration/rt_running_when_process_exits/BUILD b/tests/integration/rt_running_when_process_exits/BUILD new file mode 100644 index 000000000..4f69b1e5e --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/BUILD @@ -0,0 +1,50 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +load("//tests/utils/bazel:integration.bzl", "integration_test") + +cc_binary( + name = "filesystem_reader", + srcs = ["filesystem_reader.cpp"], + deps = [ + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "//tests/utils/test_helper:process_utils", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "control_client_test_driver", + srcs = ["control_client_test_driver.cpp"], + deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +integration_test( + name = "rt_running_when_process_exits", + timeout = "short", + srcs = ["rt_running_when_process_exits.py"], + binaries = [ + ":control_client_test_driver", + ":filesystem_reader", + ":setup_filesystem.sh", + ":slow_setup.sh", + "//score/launch_manager", + ], + config = ":rt_running_when_process_exits.json", +) diff --git a/tests/integration/rt_running_when_process_exits/control_client_test_driver.cpp b/tests/integration/rt_running_when_process_exits/control_client_test_driver.cpp new file mode 100644 index 000000000..cedaa45bb --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/control_client_test_driver.cpp @@ -0,0 +1,87 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/test_helper.hpp" +#include +#include + +namespace +{ +/// @brief Marker file written by slow_setup.sh once it has finished (and is about to exit). +constexpr std::string_view kSlowSetupOutput = "slow_setup_output.txt"; +} // namespace + +// Given a configuration with two run targets, each pulling in a self-terminating component whose +// ready condition is "Terminated" but which differ in whether that component has a dependent: +// +// - run_target_reader: filesystem_reader (ready "Running") depends on setup_filesystem_sh +// (self-terminating, ready "Terminated"). The terminated-ready +// component HAS a dependent. +// - run_target_slow_setup: depends directly on slow_setup_sh (self-terminating, ready +// "Terminated") which has NO dependent component. +// +// In both cases the run target must only report success once the terminated-ready component's +// process has actually exited. Without the fix, graph accounting for such a node happens as soon as +// the process is *started*, so ActivateRunTarget(...).Get() returns while the script is still +// running and its marker file has not been written yet. +TEST(RtRunningWhenProcessExits, ControlClientTestDriver) +{ + score::mw::lifecycle::ControlClient client; + score::cpp::stop_token stop_token; + + // kSlowSetupOutput is checked too: its later presence must be a reliable signal that + // slow_setup.sh terminated during *this* run, not leftover from a previous one. + ASSERT_TRUE(check_clean({test_end_location, kSlowSetupOutput})); + + TEST_STEP("Report running") + { + score::mw::lifecycle::report_running(); + } + + // The with-dependents case: filesystem_reader asserts on the prepared file and on the setup + // script process being gone, so the ordering is checked there. + TEST_STEP("Activate run target with a terminated-ready component that HAS a dependent") + { + auto result = client.ActivateRunTarget("run_target_reader").Get(stop_token); + EXPECT_TRUE(result.has_value()) << "Activating run_target_reader failed: " << result.error().Message(); + } + + // The no-dependents case: activation must only complete once slow_setup.sh has terminated. + TEST_STEP("Activate run target with a terminated-ready component that has NO dependent") + { + auto result = client.ActivateRunTarget("run_target_slow_setup").Get(stop_token); + EXPECT_TRUE(result.has_value()) << "Activating run_target_slow_setup failed: " << result.error().Message(); + } + + TEST_STEP("Verify slow_setup.sh had terminated before activation completed") + { + EXPECT_TRUE(std::filesystem::exists(kSlowSetupOutput)) + << "run_target_slow_setup reported success while slow_setup.sh was still running: its " + "output file has not been written yet. A run target depending on a terminated-ready " + "component must only become ready once that component's process has actually exited."; + } + + TEST_STEP("Activate run target Off") + { + client.ActivateRunTarget("Off"); + } +} + +int main() +{ + return TestRunner(__FILE__, TerminationBehavior::kWait, TerminationNotification::kTestEnd).RunTests(); +} diff --git a/tests/integration/rt_running_when_process_exits/filesystem_reader.cpp b/tests/integration/rt_running_when_process_exits/filesystem_reader.cpp new file mode 100644 index 000000000..5b54bb75b --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/filesystem_reader.cpp @@ -0,0 +1,75 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/process_utils.hpp" +#include "tests/utils/test_helper/test_helper.hpp" +#include + +namespace +{ + +constexpr std::string_view kSetupScriptName = "setup_filesystem.sh"; +constexpr std::string_view kSetupOutputFile = "setup_filesystem_output.txt"; + +} // namespace + +// Given a configuration with: +// - A self-terminating component "setup_filesystem_sh" (wrapping setup_filesystem.sh) whose +// ready condition is "Terminated". +// - A component "filesystem_reader" that depends on "setup_filesystem_sh". +// - An initial Run Target "Startup" that depends on "filesystem_reader". +// +// When the Launch Manager activates "Startup", it must first run setup_filesystem.sh to completion +// (the script writes a marker file and exits) before starting filesystem_reader. +TEST(RtRunningWhenProcessExits, FilesystemReader) +{ + ASSERT_TRUE(check_clean({test_end_location})); + + TEST_STEP("Report running") + { + score::mw::lifecycle::report_running(); + } + + TEST_STEP("Read file prepared by setup_filesystem.sh") + { + ASSERT_TRUE(std::filesystem::exists(kSetupOutputFile)) + << "The file prepared by setup_filesystem.sh does not exist; the dependency was not " + "started/finished before filesystem_reader"; + + std::ifstream output{std::filesystem::path{kSetupOutputFile}}; + ASSERT_TRUE(output.is_open()) << "Could not open " << kSetupOutputFile; + std::string content; + std::getline(output, content); + EXPECT_EQ(content, "filesystem is ready") << "Unexpected content in " << kSetupOutputFile; + } + + TEST_STEP("Verify setup_filesystem.sh process has already terminated") + { + EXPECT_FALSE(test_helper::process_is_running(kSetupScriptName)) + << "The setup_filesystem.sh process is still running; filesystem_reader was started " + "before its dependency terminated"; + } +} + +int main() +{ + // test_end is signalled by control_client_test_driver, which orchestrates the run target switches. + TestRunner runner{__FILE__, TerminationBehavior::kContinue, TerminationNotification::kNone}; + return runner.RunTests(); +} diff --git a/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.json b/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.json new file mode 100644 index 000000000..c251de205 --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.json @@ -0,0 +1,129 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/rt_running_when_process_exits", + "ready_timeout": 2.0, + "shutdown_timeout": 1.0, + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0 + } + }, + "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": true + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_client_test_driver": { + "description": "Drives the test: activates the run targets in sequence and checks that a terminated-ready component with no dependents has actually terminated before its run target reports success.", + "component_properties": { + "binary_name": "control_client_test_driver", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0 + } + } + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": { + "PROCESSIDENTIFIER": "control_client_test_driver" + } + } + }, + "setup_filesystem_sh": { + "description": "Wraps setup_filesystem.sh which prepares the filesystem (here: writes a file) and then terminates. The component becomes ready only once the script process has terminated successfully. It HAS a dependent (filesystem_reader), so its Terminated ready condition is carried on the dependency edge.", + "component_properties": { + "binary_name": "setup_filesystem.sh", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "depends_on": [], + "ready_condition": { + "process_state": "Terminated" + } + } + }, + "filesystem_reader": { + "description": "C++ executable that reads the file prepared by setup_filesystem.sh and verifies that the script process has already terminated.", + "component_properties": { + "binary_name": "filesystem_reader", + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": true + }, + "depends_on": [ + "setup_filesystem_sh" + ], + "ready_condition": { + "process_state": "Running" + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "filesystem_reader" + } + } + }, + "slow_setup_sh": { + "description": "Wraps slow_setup.sh: a self-terminating component whose ready condition is Terminated but which has NO dependent component. It waits briefly before writing its marker file. This reproduces the bug where a run target depending directly on such a component reports success as soon as the process is started, instead of once it has terminated.", + "component_properties": { + "binary_name": "slow_setup.sh", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "depends_on": [], + "ready_condition": { + "process_state": "Terminated" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_client_test_driver" + ] + }, + "run_target_reader": { + "depends_on": [ + "control_client_test_driver", + "filesystem_reader" + ] + }, + "run_target_slow_setup": { + "depends_on": [ + "control_client_test_driver", + "slow_setup_sh" + ] + }, + "Off": { + "depends_on": [] + } + }, + "initial_run_target": "Startup", + "fallback_run_target": { + "depends_on": [] + } +} diff --git a/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.py b/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.py new file mode 100644 index 000000000..4bb8addc5 --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/rt_running_when_process_exits.py @@ -0,0 +1,65 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +from tests.utils.testing_utils.run_until_file_deployed import run_until_file_deployed +from tests.utils.testing_utils.setup_test import setup_test +from tests.utils.testing_utils.test_results import assert_test_results +from attribute_plugin import add_test_properties + + +@add_test_properties( + partially_verifies=[ + "feat_req__lifecycle__start_named_run_target", + "feat_req__lifecycle__launch_support", + "comp_req__launch_man__process_state_comm", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_rt_running_when_process_exits( + target, setup_test, assert_test_results, remote_test_dir +): + """ + Objective: Verifies that a Run Target becomes ready only once a self-terminating component's + process has actually exited, both when the terminated-ready component has a dependent and when + it has none. + + A control client (control_client_test_driver) activates two run targets in sequence: + - run_target_reader: filesystem_reader (ready "Running") depends on setup_filesystem_sh + (self-terminating, ready "Terminated"). The terminated-ready + component HAS a dependent, so filesystem_reader finds the prepared + file and confirms the script process is gone. + - run_target_slow_setup: depends directly on slow_setup_sh (self-terminating, ready + "Terminated") which has NO dependent. slow_setup.sh waits briefly + before writing its marker file; the control client verifies that + the marker exists once activation completes, i.e. the run target + only became ready once the process had terminated. + + Expected Behaviour: Both activations complete only after the respective terminated-ready + component's process has exited. + """ + + # 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/rt_running_when_process_exits.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=8.0, + ) + + assert_test_results({"control_client_test_driver.xml", "filesystem_reader.xml"}) diff --git a/tests/integration/rt_running_when_process_exits/setup_filesystem.sh b/tests/integration/rt_running_when_process_exits/setup_filesystem.sh new file mode 100644 index 000000000..bf2d60ac1 --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/setup_filesystem.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# ******************************************************************************* +# 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 +# ******************************************************************************* + +# This script stands in for a real filesystem setup step (e.g. mounting +# partitions). Instead of mounting anything it simply writes a marker file that +# a dependent component reads later. It is launched by the Launch Manager as a +# self-terminating component whose ready condition is "Terminated": the Launch +# Manager only considers it ready once this script has exited successfully. + +echo "filesystem is ready" > setup_filesystem_output.txt + +exit 0 diff --git a/tests/integration/rt_running_when_process_exits/slow_setup.sh b/tests/integration/rt_running_when_process_exits/slow_setup.sh new file mode 100644 index 000000000..c0cf957c9 --- /dev/null +++ b/tests/integration/rt_running_when_process_exits/slow_setup.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# ******************************************************************************* +# 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 +# ******************************************************************************* + +# This script stands in for a slow, self-terminating filesystem setup step whose +# component has ready condition "Terminated" and NO dependent component. It waits +# for a moment before writing its marker file so that a run target depending +# directly on it can be observed reporting success *before* the process has +# actually terminated (i.e. before the marker file exists) when the deferral of +# graph accounting until termination is not implemented. + +sleep 1 + +echo "slow setup done" > slow_setup_output.txt + +exit 0 diff --git a/tests/utils/test_helper/BUILD b/tests/utils/test_helper/BUILD index c044cce2c..1c79cea1c 100644 --- a/tests/utils/test_helper/BUILD +++ b/tests/utils/test_helper/BUILD @@ -33,6 +33,12 @@ cc_library( ], ) +cc_library( + name = "process_utils", + hdrs = ["process_utils.hpp"], + visibility = ["//tests:__subpackages__"], +) + cc_binary( name = "reporting_process", srcs = ["reporting_process.cpp"], diff --git a/tests/utils/test_helper/process_utils.hpp b/tests/utils/test_helper/process_utils.hpp new file mode 100644 index 000000000..8d260e979 --- /dev/null +++ b/tests/utils/test_helper/process_utils.hpp @@ -0,0 +1,108 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef TESTS_UTILS_TEST_HELPER_PROCESS_UTILS_HPP +#define TESTS_UTILS_TEST_HELPER_PROCESS_UTILS_HPP + +#include +#include +#include +#include +#include + +#ifdef __QNXNTO__ +#include +#include +#include +#include +#include +#else +#include +#include +#endif + +namespace test_helper::detail +{ +// Not tested +#ifdef __QNXNTO__ +/// @brief Returns the executable path of the process owning @p proc_entry (e.g. /proc/1234). +/// @details QNX has no /proc//cmdline, so the executable path is read via the procfs debug interface. +inline std::string process_identity(const std::filesystem::path& proc_entry) +{ + const int fd = ::open((proc_entry / "as").c_str(), O_RDONLY); + if (fd == -1) + { + return {}; + } + + struct + { + procfs_debuginfo info; + char path_buffer[PATH_MAX]; + } map{}; + + std::string identity; + if (::devctl(fd, DCMD_PROC_MAPDEBUG_BASE, &map, sizeof(map), nullptr) == EOK) + { + identity = map.info.path; + } + ::close(fd); + return identity; +} +#else +/// @brief Returns the null-separated command line of the process owning @p proc_entry (e.g. /proc/1234). +inline std::string process_identity(const std::filesystem::path& proc_entry) +{ + std::ifstream cmdline{proc_entry / "cmdline", std::ios::binary}; + if (!cmdline) + { + return {}; // Process may have vanished between listing and reading. + } + std::stringstream buffer; + buffer << cmdline.rdbuf(); + return buffer.str(); +} +#endif +} // namespace test_helper::detail + +namespace test_helper +{ +/// @brief Returns true if any currently running process was launched from @p process_name. +/// @details Walks /proc and matches @p process_name against each process's command line (Linux) or +/// executable path (QNX). Zombie/reaped processes carry no identity and are therefore ignored. +inline bool process_is_running(const std::string_view process_name) +{ + for (const auto& entry : std::filesystem::directory_iterator{"/proc"}) + { + if (!entry.is_directory()) + { + continue; + } + + const std::string pid = entry.path().filename().string(); + if (pid.empty() || !std::all_of(pid.begin(), pid.end(), [](unsigned char c) { + return std::isdigit(c); + })) + { + continue; // Not a process directory. + } + + if (detail::process_identity(entry.path()).find(process_name) != std::string::npos) + { + return true; + } + } + return false; +} +} // namespace test_helper + +#endif // TESTS_UTILS_TEST_HELPER_PROCESS_UTILS_HPP