diff --git a/examples/README.md b/examples/README.md index a4084fc2c..7cfe0a485 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,6 +24,7 @@ The test verifies the following scenarios end-to-end: | `cpp_supervised_app` | C++ app with alive supervision and crash recovery | | `rust_supervised_app` | Rust app with alive supervision and crash recovery | | `cpp_lifecycle_app` | C++ app demonstrating basic lifecycle management | +| `cpp_supervised_app_simple` | C++ app demonstrating the plain `Alive` API (report Running, then periodic `ReportAlive()` heartbeats) without deadline/checkpoint supervision | | `control_daemon` | State Manager app for requesting RunTarget transitions | ## Launch Manager Configuration diff --git a/examples/cpp_supervised_app_simple/BUILD b/examples/cpp_supervised_app_simple/BUILD new file mode 100644 index 000000000..6c19c658b --- /dev/null +++ b/examples/cpp_supervised_app_simple/BUILD @@ -0,0 +1,38 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_binary") + +cc_binary( + name = "cpp_supervised_app_simple", + srcs = [ + "main.cpp", + ], + copts = select({ + "@platforms//os:qnx": [], + "@platforms//os:linux": [], + }), + linkopts = select({ + "@platforms//os:qnx": [ + "-lsocket", + ], + "@platforms//os:linux": [ + "-lpthread", + "-lrt", + ], + }), + visibility = ["//visibility:public"], + deps = [ + "//score/launch_manager:alive_cc", + "//score/launch_manager:lifecycle_cc", + ], +) diff --git a/examples/cpp_supervised_app_simple/main.cpp b/examples/cpp_supervised_app_simple/main.cpp new file mode 100644 index 000000000..af677a592 --- /dev/null +++ b/examples/cpp_supervised_app_simple/main.cpp @@ -0,0 +1,126 @@ +/******************************************************************************** + * 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 + +#ifdef __linux__ +#include +#include +#endif + +#include +#include + +/// @brief CLI configuration options for the cpp_supervised_app_simple process +struct Config +{ + std::uint32_t sleepInMs{50}; +}; + +std::optional parseOptions(int argc, char* const* argv) noexcept +{ + Config config{}; + int c; + while ((c = getopt(argc, argv, "s:h")) != -1) + { + switch (static_cast(c)) + { + case 's': + config.sleepInMs = std::stoi(optarg); + break; + + case 'h': + std::cout << "Usage: \n\ + -s \n"; + return std::nullopt; + + default: + break; + } + } + return config; +} + +std::atomic exitRequested{false}; +void signalHandler(int signal) +{ + if (signal == SIGINT || signal == SIGTERM) + { + exitRequested = true; + } +} + +void set_process_name() +{ + const char* identifier = getenv("PROCESSIDENTIFIER"); + if (identifier != nullptr) + { +#if defined(__QNXNTO__) + if (pthread_setname_np(pthread_self(), identifier) != 0) + { + std::cerr << "Failed to set QNX thread name" << std::endl; + } +#elif defined(__linux__) + if (prctl(PR_SET_NAME, identifier) < 0) + { + std::cerr << "Failed to set process name to " << identifier << std::endl; + } +#endif + } +} + +/// @brief Minimal example of the Alive API: report Running once, then report +/// alive notifications periodically until the Launch Manager requests shutdown. +/// Unlike cpp_supervised_app, this example has no deadline/checkpoint +/// supervision -- it only demonstrates the plain alive heartbeat. +int main(int argc, char** argv) +{ + set_process_name(); + + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + + const auto config = parseOptions(argc, argv); + if (!config) + { + return EXIT_FAILURE; + } + + std::optional alive; + try + { + alive.emplace("cpp_supervised_app_simple"); + } + catch (const std::exception& e) + { + std::cerr << "Failed to create Alive instance: " << e.what() << std::endl; + return EXIT_FAILURE; + } + + score::mw::lifecycle::report_running(); + + while (!exitRequested) + { + alive->ReportAlive(); + std::this_thread::sleep_for(std::chrono::milliseconds(config->sleepInMs)); + } + + return EXIT_SUCCESS; +} diff --git a/examples/demo_verification/BUILD b/examples/demo_verification/BUILD index cbca3b511..171dd12f7 100644 --- a/examples/demo_verification/BUILD +++ b/examples/demo_verification/BUILD @@ -21,6 +21,7 @@ integration_test( "//examples/control_application:lmcontrol", "//examples/cpp_lifecycle_app", "//examples/cpp_supervised_app", + "//examples/cpp_supervised_app_simple", "//examples/rust_supervised_app", "//score/launch_manager", ], diff --git a/examples/demo_verification/lifecycle_demo_test.json b/examples/demo_verification/lifecycle_demo_test.json index 0ad84cdb4..48dd6b9b3 100644 --- a/examples/demo_verification/lifecycle_demo_test.json +++ b/examples/demo_verification/lifecycle_demo_test.json @@ -117,6 +117,20 @@ "PROCESSIDENTIFIER": "lifecycle_app" } } + }, + "cpp_supervised_app_simple": { + "component_properties": { + "binary_name": "cpp_supervised_app_simple", + "process_arguments": [ + "-s50" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "cpp_supervised_app_simple", + "IDENTIFIER": "cpp_supervised_app_simple" + } + } } }, "run_targets": { @@ -136,7 +150,8 @@ "cpp_supervised_app", "rust_supervised_app", "c_supervised_app", - "lifecycle_app" + "lifecycle_app", + "cpp_supervised_app_simple" ], "recovery_action": { "switch_run_target": { diff --git a/examples/demo_verification/test_examples.py b/examples/demo_verification/test_examples.py index 70bdc7f7b..f3eaa0219 100644 --- a/examples/demo_verification/test_examples.py +++ b/examples/demo_verification/test_examples.py @@ -20,30 +20,53 @@ "rust_supervised_app", "cpp_lifecycle_app", "c_supervised_app", + "cpp_supervised_app_simple", ) +def _cmdline_pattern(binary): + """Anchored ERE matching an exact argv token equal to `binary`, either + bare or with a path prefix (e.g. "/tmp/tests/examples/cpp_supervised_app"). + + Anchoring matters once `cpp_supervised_app_simple` is in the mix: a plain + substring search for "cpp_supervised_app" also matches inside + "cpp_supervised_app_simple"'s cmdline, which would silently break both + _assert_running (false positive) and _assert_not_running (false negative + after killing the base app) for the shorter name. + """ + return f"^({binary}|.*/{binary})$" + + +def _proc_has_binary(target, binary): + """True if any process's argv (NUL-separated in /proc/*/cmdline, turned + into one-token-per-line here) has an exact token matching `binary`.""" + pattern = _cmdline_pattern(binary) + rc, _ = target.execute( + f"cat /proc/*/cmdline 2>/dev/null | tr '\\0' '\\n' | grep -qE '{pattern}'" + ) + return rc == 0 + + def _assert_running(target, *binaries): """Assert each binary has a live entry in /proc/*/cmdline.""" for binary in binaries: - pattern = f"[{binary[0]}]{binary[1:]}" - rc, _ = target.execute(f"grep -qa '{pattern}' /proc/*/cmdline 2>/dev/null") - assert rc == 0, f"{binary} is not running" + assert _proc_has_binary(target, binary), f"{binary} is not running" def _assert_not_running(target, binary): """Assert no live /proc/*/cmdline entry exists for binary.""" - pattern = f"[{binary[0]}]{binary[1:]}" - rc, _ = target.execute(f"grep -qa '{pattern}' /proc/*/cmdline 2>/dev/null") - assert rc != 0, f"{binary} is still running; expected it to be stopped" + assert not _proc_has_binary(target, binary), ( + f"{binary} is still running; expected it to be stopped" + ) def _send_signal(target, binary, signal): """Find binary via /proc/*/cmdline and send signal in a single shell invocation.""" - pattern = f"[{binary[0]}]{binary[1:]}" + pattern = _cmdline_pattern(binary) _, pid_output = target.execute( f"p=; for c in /proc/[0-9]*/cmdline; do " - f"grep -qa '{pattern}' $c 2>/dev/null && p=$(echo $c | cut -d/ -f3) && break; " + f"tr '\\0' '\\n' < \"$c\" 2>/dev/null | grep -qE '{pattern}' " + f"&& p=$(echo $c | cut -d/ -f3) && break; " f"done; kill -{signal} $p 2>/dev/null; echo $p" ) assert pid_output.strip(), f"Failed to find {binary} to send signal {signal}"